File: ./ansi.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "fmt"
   6     "strconv"
   7 )
   8 
   9 // styledHexBytes is a super-fast direct byte-to-result lookup table, and was
  10 // autogenerated by running the command
  11 //
  12 // seq 0 255 | ./hex-styles.awk
  13 var styledHexBytes = [256]string{
  14     "\x1b[38;5;111m00 ", "\x1b[38;5;246m01 ",
  15     "\x1b[38;5;246m02 ", "\x1b[38;5;246m03 ",
  16     "\x1b[38;5;246m04 ", "\x1b[38;5;246m05 ",
  17     "\x1b[38;5;246m06 ", "\x1b[38;5;246m07 ",
  18     "\x1b[38;5;246m08 ", "\x1b[38;5;246m09 ",
  19     "\x1b[38;5;246m0a ", "\x1b[38;5;246m0b ",
  20     "\x1b[38;5;246m0c ", "\x1b[38;5;246m0d ",
  21     "\x1b[38;5;246m0e ", "\x1b[38;5;246m0f ",
  22     "\x1b[38;5;246m10 ", "\x1b[38;5;246m11 ",
  23     "\x1b[38;5;246m12 ", "\x1b[38;5;246m13 ",
  24     "\x1b[38;5;246m14 ", "\x1b[38;5;246m15 ",
  25     "\x1b[38;5;246m16 ", "\x1b[38;5;246m17 ",
  26     "\x1b[38;5;246m18 ", "\x1b[38;5;246m19 ",
  27     "\x1b[38;5;246m1a ", "\x1b[38;5;246m1b ",
  28     "\x1b[38;5;246m1c ", "\x1b[38;5;246m1d ",
  29     "\x1b[38;5;246m1e ", "\x1b[38;5;246m1f ",
  30     "\x1b[38;5;72m20\x1b[38;5;239m ", "\x1b[38;5;72m21\x1b[38;5;239m!",
  31     "\x1b[38;5;72m22\x1b[38;5;239m\"", "\x1b[38;5;72m23\x1b[38;5;239m#",
  32     "\x1b[38;5;72m24\x1b[38;5;239m$", "\x1b[38;5;72m25\x1b[38;5;239m%",
  33     "\x1b[38;5;72m26\x1b[38;5;239m&", "\x1b[38;5;72m27\x1b[38;5;239m'",
  34     "\x1b[38;5;72m28\x1b[38;5;239m(", "\x1b[38;5;72m29\x1b[38;5;239m)",
  35     "\x1b[38;5;72m2a\x1b[38;5;239m*", "\x1b[38;5;72m2b\x1b[38;5;239m+",
  36     "\x1b[38;5;72m2c\x1b[38;5;239m,", "\x1b[38;5;72m2d\x1b[38;5;239m-",
  37     "\x1b[38;5;72m2e\x1b[38;5;239m.", "\x1b[38;5;72m2f\x1b[38;5;239m/",
  38     "\x1b[38;5;72m30\x1b[38;5;239m0", "\x1b[38;5;72m31\x1b[38;5;239m1",
  39     "\x1b[38;5;72m32\x1b[38;5;239m2", "\x1b[38;5;72m33\x1b[38;5;239m3",
  40     "\x1b[38;5;72m34\x1b[38;5;239m4", "\x1b[38;5;72m35\x1b[38;5;239m5",
  41     "\x1b[38;5;72m36\x1b[38;5;239m6", "\x1b[38;5;72m37\x1b[38;5;239m7",
  42     "\x1b[38;5;72m38\x1b[38;5;239m8", "\x1b[38;5;72m39\x1b[38;5;239m9",
  43     "\x1b[38;5;72m3a\x1b[38;5;239m:", "\x1b[38;5;72m3b\x1b[38;5;239m;",
  44     "\x1b[38;5;72m3c\x1b[38;5;239m<", "\x1b[38;5;72m3d\x1b[38;5;239m=",
  45     "\x1b[38;5;72m3e\x1b[38;5;239m>", "\x1b[38;5;72m3f\x1b[38;5;239m?",
  46     "\x1b[38;5;72m40\x1b[38;5;239m@", "\x1b[38;5;72m41\x1b[38;5;239mA",
  47     "\x1b[38;5;72m42\x1b[38;5;239mB", "\x1b[38;5;72m43\x1b[38;5;239mC",
  48     "\x1b[38;5;72m44\x1b[38;5;239mD", "\x1b[38;5;72m45\x1b[38;5;239mE",
  49     "\x1b[38;5;72m46\x1b[38;5;239mF", "\x1b[38;5;72m47\x1b[38;5;239mG",
  50     "\x1b[38;5;72m48\x1b[38;5;239mH", "\x1b[38;5;72m49\x1b[38;5;239mI",
  51     "\x1b[38;5;72m4a\x1b[38;5;239mJ", "\x1b[38;5;72m4b\x1b[38;5;239mK",
  52     "\x1b[38;5;72m4c\x1b[38;5;239mL", "\x1b[38;5;72m4d\x1b[38;5;239mM",
  53     "\x1b[38;5;72m4e\x1b[38;5;239mN", "\x1b[38;5;72m4f\x1b[38;5;239mO",
  54     "\x1b[38;5;72m50\x1b[38;5;239mP", "\x1b[38;5;72m51\x1b[38;5;239mQ",
  55     "\x1b[38;5;72m52\x1b[38;5;239mR", "\x1b[38;5;72m53\x1b[38;5;239mS",
  56     "\x1b[38;5;72m54\x1b[38;5;239mT", "\x1b[38;5;72m55\x1b[38;5;239mU",
  57     "\x1b[38;5;72m56\x1b[38;5;239mV", "\x1b[38;5;72m57\x1b[38;5;239mW",
  58     "\x1b[38;5;72m58\x1b[38;5;239mX", "\x1b[38;5;72m59\x1b[38;5;239mY",
  59     "\x1b[38;5;72m5a\x1b[38;5;239mZ", "\x1b[38;5;72m5b\x1b[38;5;239m[",
  60     "\x1b[38;5;72m5c\x1b[38;5;239m\\", "\x1b[38;5;72m5d\x1b[38;5;239m]",
  61     "\x1b[38;5;72m5e\x1b[38;5;239m^", "\x1b[38;5;72m5f\x1b[38;5;239m_",
  62     "\x1b[38;5;72m60\x1b[38;5;239m`", "\x1b[38;5;72m61\x1b[38;5;239ma",
  63     "\x1b[38;5;72m62\x1b[38;5;239mb", "\x1b[38;5;72m63\x1b[38;5;239mc",
  64     "\x1b[38;5;72m64\x1b[38;5;239md", "\x1b[38;5;72m65\x1b[38;5;239me",
  65     "\x1b[38;5;72m66\x1b[38;5;239mf", "\x1b[38;5;72m67\x1b[38;5;239mg",
  66     "\x1b[38;5;72m68\x1b[38;5;239mh", "\x1b[38;5;72m69\x1b[38;5;239mi",
  67     "\x1b[38;5;72m6a\x1b[38;5;239mj", "\x1b[38;5;72m6b\x1b[38;5;239mk",
  68     "\x1b[38;5;72m6c\x1b[38;5;239ml", "\x1b[38;5;72m6d\x1b[38;5;239mm",
  69     "\x1b[38;5;72m6e\x1b[38;5;239mn", "\x1b[38;5;72m6f\x1b[38;5;239mo",
  70     "\x1b[38;5;72m70\x1b[38;5;239mp", "\x1b[38;5;72m71\x1b[38;5;239mq",
  71     "\x1b[38;5;72m72\x1b[38;5;239mr", "\x1b[38;5;72m73\x1b[38;5;239ms",
  72     "\x1b[38;5;72m74\x1b[38;5;239mt", "\x1b[38;5;72m75\x1b[38;5;239mu",
  73     "\x1b[38;5;72m76\x1b[38;5;239mv", "\x1b[38;5;72m77\x1b[38;5;239mw",
  74     "\x1b[38;5;72m78\x1b[38;5;239mx", "\x1b[38;5;72m79\x1b[38;5;239my",
  75     "\x1b[38;5;72m7a\x1b[38;5;239mz", "\x1b[38;5;72m7b\x1b[38;5;239m{",
  76     "\x1b[38;5;72m7c\x1b[38;5;239m|", "\x1b[38;5;72m7d\x1b[38;5;239m}",
  77     "\x1b[38;5;72m7e\x1b[38;5;239m~", "\x1b[38;5;246m7f ",
  78     "\x1b[38;5;246m80 ", "\x1b[38;5;246m81 ",
  79     "\x1b[38;5;246m82 ", "\x1b[38;5;246m83 ",
  80     "\x1b[38;5;246m84 ", "\x1b[38;5;246m85 ",
  81     "\x1b[38;5;246m86 ", "\x1b[38;5;246m87 ",
  82     "\x1b[38;5;246m88 ", "\x1b[38;5;246m89 ",
  83     "\x1b[38;5;246m8a ", "\x1b[38;5;246m8b ",
  84     "\x1b[38;5;246m8c ", "\x1b[38;5;246m8d ",
  85     "\x1b[38;5;246m8e ", "\x1b[38;5;246m8f ",
  86     "\x1b[38;5;246m90 ", "\x1b[38;5;246m91 ",
  87     "\x1b[38;5;246m92 ", "\x1b[38;5;246m93 ",
  88     "\x1b[38;5;246m94 ", "\x1b[38;5;246m95 ",
  89     "\x1b[38;5;246m96 ", "\x1b[38;5;246m97 ",
  90     "\x1b[38;5;246m98 ", "\x1b[38;5;246m99 ",
  91     "\x1b[38;5;246m9a ", "\x1b[38;5;246m9b ",
  92     "\x1b[38;5;246m9c ", "\x1b[38;5;246m9d ",
  93     "\x1b[38;5;246m9e ", "\x1b[38;5;246m9f ",
  94     "\x1b[38;5;246ma0 ", "\x1b[38;5;246ma1 ",
  95     "\x1b[38;5;246ma2 ", "\x1b[38;5;246ma3 ",
  96     "\x1b[38;5;246ma4 ", "\x1b[38;5;246ma5 ",
  97     "\x1b[38;5;246ma6 ", "\x1b[38;5;246ma7 ",
  98     "\x1b[38;5;246ma8 ", "\x1b[38;5;246ma9 ",
  99     "\x1b[38;5;246maa ", "\x1b[38;5;246mab ",
 100     "\x1b[38;5;246mac ", "\x1b[38;5;246mad ",
 101     "\x1b[38;5;246mae ", "\x1b[38;5;246maf ",
 102     "\x1b[38;5;246mb0 ", "\x1b[38;5;246mb1 ",
 103     "\x1b[38;5;246mb2 ", "\x1b[38;5;246mb3 ",
 104     "\x1b[38;5;246mb4 ", "\x1b[38;5;246mb5 ",
 105     "\x1b[38;5;246mb6 ", "\x1b[38;5;246mb7 ",
 106     "\x1b[38;5;246mb8 ", "\x1b[38;5;246mb9 ",
 107     "\x1b[38;5;246mba ", "\x1b[38;5;246mbb ",
 108     "\x1b[38;5;246mbc ", "\x1b[38;5;246mbd ",
 109     "\x1b[38;5;246mbe ", "\x1b[38;5;246mbf ",
 110     "\x1b[38;5;246mc0 ", "\x1b[38;5;246mc1 ",
 111     "\x1b[38;5;246mc2 ", "\x1b[38;5;246mc3 ",
 112     "\x1b[38;5;246mc4 ", "\x1b[38;5;246mc5 ",
 113     "\x1b[38;5;246mc6 ", "\x1b[38;5;246mc7 ",
 114     "\x1b[38;5;246mc8 ", "\x1b[38;5;246mc9 ",
 115     "\x1b[38;5;246mca ", "\x1b[38;5;246mcb ",
 116     "\x1b[38;5;246mcc ", "\x1b[38;5;246mcd ",
 117     "\x1b[38;5;246mce ", "\x1b[38;5;246mcf ",
 118     "\x1b[38;5;246md0 ", "\x1b[38;5;246md1 ",
 119     "\x1b[38;5;246md2 ", "\x1b[38;5;246md3 ",
 120     "\x1b[38;5;246md4 ", "\x1b[38;5;246md5 ",
 121     "\x1b[38;5;246md6 ", "\x1b[38;5;246md7 ",
 122     "\x1b[38;5;246md8 ", "\x1b[38;5;246md9 ",
 123     "\x1b[38;5;246mda ", "\x1b[38;5;246mdb ",
 124     "\x1b[38;5;246mdc ", "\x1b[38;5;246mdd ",
 125     "\x1b[38;5;246mde ", "\x1b[38;5;246mdf ",
 126     "\x1b[38;5;246me0 ", "\x1b[38;5;246me1 ",
 127     "\x1b[38;5;246me2 ", "\x1b[38;5;246me3 ",
 128     "\x1b[38;5;246me4 ", "\x1b[38;5;246me5 ",
 129     "\x1b[38;5;246me6 ", "\x1b[38;5;246me7 ",
 130     "\x1b[38;5;246me8 ", "\x1b[38;5;246me9 ",
 131     "\x1b[38;5;246mea ", "\x1b[38;5;246meb ",
 132     "\x1b[38;5;246mec ", "\x1b[38;5;246med ",
 133     "\x1b[38;5;246mee ", "\x1b[38;5;246mef ",
 134     "\x1b[38;5;246mf0 ", "\x1b[38;5;246mf1 ",
 135     "\x1b[38;5;246mf2 ", "\x1b[38;5;246mf3 ",
 136     "\x1b[38;5;246mf4 ", "\x1b[38;5;246mf5 ",
 137     "\x1b[38;5;246mf6 ", "\x1b[38;5;246mf7 ",
 138     "\x1b[38;5;246mf8 ", "\x1b[38;5;246mf9 ",
 139     "\x1b[38;5;246mfa ", "\x1b[38;5;246mfb ",
 140     "\x1b[38;5;246mfc ", "\x1b[38;5;246mfd ",
 141     "\x1b[38;5;246mfe ", "\x1b[38;5;209mff ",
 142 }
 143 
 144 // hexSymbols is a direct lookup table combining 2 hex digits with either a
 145 // space or a displayable ASCII symbol matching the byte's own ASCII value;
 146 // this table was autogenerated by running the command
 147 //
 148 // seq 0 255 | ./hex-symbols.awk
 149 var hexSymbols = [256]string{
 150     `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
 151     `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
 152     `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
 153     `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
 154     `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
 155     `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
 156     `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
 157     `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
 158     `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
 159     `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
 160     `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
 161     `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
 162     "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
 163     `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
 164     `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
 165     `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
 166     `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
 167     `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
 168     `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
 169     `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
 170     `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
 171     `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
 172     `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
 173     `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
 174     `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
 175     `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
 176     `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
 177     `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
 178     `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
 179     `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
 180     `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
 181     `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
 182 }
 183 
 184 const (
 185     unknownStyle = 0
 186     zeroStyle    = 1
 187     otherStyle   = 2
 188     asciiStyle   = 3
 189     allOnStyle   = 4
 190 )
 191 
 192 // byteStyles turns bytes into one of several distinct visual types, which
 193 // allows quickly telling when ANSI styles codes are repetitive and when
 194 // they're actually needed
 195 var byteStyles = [256]int{
 196     zeroStyle, otherStyle, otherStyle, otherStyle,
 197     otherStyle, otherStyle, otherStyle, otherStyle,
 198     otherStyle, otherStyle, otherStyle, otherStyle,
 199     otherStyle, otherStyle, otherStyle, otherStyle,
 200     otherStyle, otherStyle, otherStyle, otherStyle,
 201     otherStyle, otherStyle, otherStyle, otherStyle,
 202     otherStyle, otherStyle, otherStyle, otherStyle,
 203     otherStyle, otherStyle, otherStyle, otherStyle,
 204     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 205     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 206     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 207     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 208     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 209     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 210     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 211     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 212     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 213     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 214     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 215     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 216     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 217     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 218     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 219     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 220     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 221     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 222     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 223     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 224     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 225     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 226     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 227     asciiStyle, asciiStyle, asciiStyle, otherStyle,
 228     otherStyle, otherStyle, otherStyle, otherStyle,
 229     otherStyle, otherStyle, otherStyle, otherStyle,
 230     otherStyle, otherStyle, otherStyle, otherStyle,
 231     otherStyle, otherStyle, otherStyle, otherStyle,
 232     otherStyle, otherStyle, otherStyle, otherStyle,
 233     otherStyle, otherStyle, otherStyle, otherStyle,
 234     otherStyle, otherStyle, otherStyle, otherStyle,
 235     otherStyle, otherStyle, otherStyle, otherStyle,
 236     otherStyle, otherStyle, otherStyle, otherStyle,
 237     otherStyle, otherStyle, otherStyle, otherStyle,
 238     otherStyle, otherStyle, otherStyle, otherStyle,
 239     otherStyle, otherStyle, otherStyle, otherStyle,
 240     otherStyle, otherStyle, otherStyle, otherStyle,
 241     otherStyle, otherStyle, otherStyle, otherStyle,
 242     otherStyle, otherStyle, otherStyle, otherStyle,
 243     otherStyle, otherStyle, otherStyle, otherStyle,
 244     otherStyle, otherStyle, otherStyle, otherStyle,
 245     otherStyle, otherStyle, otherStyle, otherStyle,
 246     otherStyle, otherStyle, otherStyle, otherStyle,
 247     otherStyle, otherStyle, otherStyle, otherStyle,
 248     otherStyle, otherStyle, otherStyle, otherStyle,
 249     otherStyle, otherStyle, otherStyle, otherStyle,
 250     otherStyle, otherStyle, otherStyle, otherStyle,
 251     otherStyle, otherStyle, otherStyle, otherStyle,
 252     otherStyle, otherStyle, otherStyle, otherStyle,
 253     otherStyle, otherStyle, otherStyle, otherStyle,
 254     otherStyle, otherStyle, otherStyle, otherStyle,
 255     otherStyle, otherStyle, otherStyle, otherStyle,
 256     otherStyle, otherStyle, otherStyle, otherStyle,
 257     otherStyle, otherStyle, otherStyle, otherStyle,
 258     otherStyle, otherStyle, otherStyle, otherStyle,
 259     otherStyle, otherStyle, otherStyle, allOnStyle,
 260 }
 261 
 262 // writeMetaANSI shows metadata right before the ANSI-styled hex byte-view
 263 func writeMetaANSI(w *bufio.Writer, fname string, fsize int, cfg config) {
 264     if cfg.Title != "" {
 265         fmt.Fprintf(w, "\x1b[4m%s\x1b[0m\n", cfg.Title)
 266         w.WriteString("\n")
 267     }
 268 
 269     if fsize < 0 {
 270         fmt.Fprintf(w, "• %s\n", fname)
 271     } else {
 272         const fs = "• %s  \x1b[38;5;245m(%s bytes)\x1b[0m\n"
 273         fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
 274     }
 275 
 276     if cfg.Skip > 0 {
 277         const fs = "   \x1b[38;5;5mskipping first %s bytes\x1b[0m\n"
 278         fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
 279     }
 280     if cfg.MaxBytes > 0 {
 281         const fs = "   \x1b[38;5;5mshowing only up to %s bytes\x1b[0m\n"
 282         fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
 283     }
 284     w.WriteString("\n")
 285 }
 286 
 287 // writeBufferANSI shows the hex byte-view using ANSI colors/styles
 288 func writeBufferANSI(rc rendererConfig, first, second []byte) error {
 289     // show a ruler every few lines to make eye-scanning easier
 290     if rc.chunks%5 == 0 && rc.chunks > 0 {
 291         writeRulerANSI(rc)
 292     }
 293 
 294     return writeLineANSI(rc, first, second)
 295 }
 296 
 297 // writeRulerANSI emits an indented ANSI-styled line showing spaced-out dots,
 298 // so as to help eye-scan items on nearby output lines
 299 func writeRulerANSI(rc rendererConfig) {
 300     w := rc.out
 301     if len(rc.ruler) == 0 {
 302         w.WriteByte('\n')
 303         return
 304     }
 305 
 306     w.WriteString("\x1b[38;5;245m")
 307     indent := int(rc.offsetWidth) + len(padding)
 308     writeSpaces(w, indent)
 309     w.Write(rc.ruler)
 310     w.WriteString("\x1b[0m\n")
 311 }
 312 
 313 func writeLineANSI(rc rendererConfig, first, second []byte) error {
 314     w := rc.out
 315 
 316     // start each line with the byte-offset for the 1st item shown on it
 317     if rc.showOffsets {
 318         writeStyledCounter(w, int(rc.offsetWidth), rc.offset)
 319         w.WriteString(padding + "\x1b[48;5;254m")
 320     } else {
 321         w.WriteString(padding)
 322     }
 323 
 324     prevStyle := unknownStyle
 325     for _, b := range first {
 326         // using the slow/generic fmt.Fprintf is a performance bottleneck,
 327         // since it's called for each input byte
 328         // w.WriteString(styledHexBytes[b])
 329 
 330         // this more complicated way of emitting output avoids repeating
 331         // ANSI styles when dealing with bytes which aren't displayable
 332         // ASCII symbols, thus emitting fewer bytes when dealing with
 333         // general binary datasets; it makes no difference for plain-text
 334         // ASCII input
 335         style := byteStyles[b]
 336         if style != prevStyle {
 337             w.WriteString(styledHexBytes[b])
 338             if style == asciiStyle {
 339                 // styling displayable ASCII symbols uses multiple different
 340                 // styles each time it happens, always forcing ANSI-style
 341                 // updates
 342                 style = unknownStyle
 343             }
 344         } else {
 345             w.WriteString(hexSymbols[b])
 346         }
 347         prevStyle = style
 348     }
 349 
 350     w.WriteString("\x1b[0m")
 351     if rc.showASCII {
 352         writePlainASCII(w, first, second, int(rc.perLine))
 353     }
 354 
 355     return w.WriteByte('\n')
 356 }
 357 
 358 func writeStyledCounter(w *bufio.Writer, width int, n uint) {
 359     var buf [32]byte
 360     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 361 
 362     // left-pad the final result with leading spaces
 363     writeSpaces(w, width-len(str))
 364 
 365     var style bool
 366     // emit leading part with 1 or 2 digits unstyled, ensuring the
 367     // rest or the rendered number's string is a multiple of 3 long
 368     if rem := len(str) % 3; rem != 0 {
 369         w.Write(str[:rem])
 370         str = str[rem:]
 371         // next digit-group needs some styling
 372         style = true
 373     } else {
 374         style = false
 375     }
 376 
 377     // alternate between styled/unstyled 3-digit groups
 378     for len(str) > 0 {
 379         if !style {
 380             w.Write(str[:3])
 381         } else {
 382             w.WriteString("\x1b[38;5;243m")
 383             w.Write(str[:3])
 384             w.WriteString("\x1b[0m")
 385         }
 386 
 387         style = !style
 388         str = str[3:]
 389     }
 390 }

     File: ./config.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "flag"
   7     "fmt"
   8 )
   9 
  10 const (
  11     usageMaxBytes   = `limit input up to n bytes; negative to disable`
  12     usagePerLine    = `how many bytes to show on each line`
  13     usageSkip       = `how many leading bytes to skip/ignore`
  14     usageTitle      = `use this to show a title/description`
  15     usageTo         = `the output format to use (plain or ansi)`
  16     usagePlain      = `show plain-text output, as opposed to ansi-styled output`
  17     usageShowOffset = `start lines with the offset of the 1st byte shown on each`
  18     usageShowASCII  = `repeat all ASCII strings on the side, so they're searcheable`
  19 )
  20 
  21 const defaultOffsetCounterWidth = 8
  22 
  23 const (
  24     plainOutput = `plain`
  25     ansiOutput  = `ansi`
  26 )
  27 
  28 // config is the parsed cmd-line options given to the app
  29 type config struct {
  30     // MaxBytes limits how many bytes are shown; a negative value means no limit
  31     MaxBytes int
  32 
  33     // PerLine is how many bytes are shown per output line
  34     PerLine int
  35 
  36     // Skip is how many leading bytes to skip/ignore
  37     Skip int
  38 
  39     // OffsetCounterWidth is the max string-width; not exposed as a cmd-line option
  40     OffsetCounterWidth uint
  41 
  42     // Title is an optional title preceding the output proper
  43     Title string
  44 
  45     // To is the output format
  46     To string
  47 
  48     // Filenames is the list of input filenames
  49     Filenames []string
  50 
  51     // Ruler is a prerendered ruler to emit every few output lines
  52     Ruler []byte
  53 
  54     // ShowOffsets starts lines with the offset of the 1st byte shown on each
  55     ShowOffsets bool
  56 
  57     // ShowASCII shows a side-panel with searcheable ASCII-runs
  58     ShowASCII bool
  59 }
  60 
  61 // parseFlags is the constructor for type config
  62 func parseFlags(usage string) config {
  63     flag.Usage = func() {
  64         fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
  65         flag.PrintDefaults()
  66     }
  67 
  68     cfg := config{
  69         MaxBytes:           -1,
  70         PerLine:            16,
  71         OffsetCounterWidth: 0,
  72         To:                 ansiOutput,
  73         ShowOffsets:        true,
  74         ShowASCII:          true,
  75     }
  76 
  77     plain := false
  78     flag.IntVar(&cfg.MaxBytes, `max`, cfg.MaxBytes, usageMaxBytes)
  79     flag.IntVar(&cfg.PerLine, `width`, cfg.PerLine, usagePerLine)
  80     flag.IntVar(&cfg.Skip, `skip`, cfg.Skip, usageSkip)
  81     flag.StringVar(&cfg.Title, `title`, cfg.Title, usageTitle)
  82     flag.StringVar(&cfg.To, `to`, cfg.To, usageTo)
  83     flag.BoolVar(&cfg.ShowOffsets, `n`, cfg.ShowOffsets, usageShowOffset)
  84     flag.BoolVar(&cfg.ShowASCII, `ascii`, cfg.ShowASCII, usageShowASCII)
  85     flag.BoolVar(&plain, `p`, plain, "alias for option `plain`")
  86     flag.BoolVar(&plain, `plain`, plain, usagePlain)
  87     flag.Parse()
  88 
  89     if plain {
  90         cfg.To = plainOutput
  91     }
  92 
  93     // normalize values for option -to
  94     switch cfg.To {
  95     case `text`, `plaintext`, `plain-text`:
  96         cfg.To = plainOutput
  97     }
  98 
  99     cfg.Ruler = makeRuler(cfg.PerLine)
 100     cfg.Filenames = flag.Args()
 101     return cfg
 102 }
 103 
 104 // makeRuler prerenders a ruler-line, used to make the output lines breathe
 105 func makeRuler(numitems int) []byte {
 106     if n := numitems / 4; n > 0 {
 107         var pat = []byte(`           ·`)
 108         return bytes.Repeat(pat, n)
 109     }
 110     return nil
 111 }
 112 
 113 // rendererConfig groups several arguments given to any of the rendering funcs
 114 type rendererConfig struct {
 115     // out is writer to send all output to
 116     out *bufio.Writer
 117 
 118     // offset is the byte-offset of the first byte shown on the current output
 119     // line: if shown at all, it's shown at the start the line
 120     offset uint
 121 
 122     // chunks is the 0-based counter for byte-chunks/lines shown so far, which
 123     // indirectly keeps track of when it's time to show a `breather` line
 124     chunks uint
 125 
 126     // ruler is the `ruler` content to show on `breather` lines
 127     ruler []byte
 128 
 129     // perLine is how many hex-encoded bytes are shown per line
 130     perLine uint
 131 
 132     // offsetWidth is the max string-width for the byte-offsets shown at the
 133     // start of output lines, and determines those values' left-padding
 134     offsetWidth uint
 135 
 136     // showOffsets determines whether byte-offsets are shown at all
 137     showOffsets bool
 138 
 139     // showASCII determines whether the ASCII-panels are shown at all
 140     showASCII bool
 141 }

     File: ./go.mod
   1 module hex
   2 
   3 go 1.18

     File: ./hex-styles.awk
   1 #!/usr/bin/awk -f
   2 
   3 # all 0 bits
   4 $0 == 0 {
   5     print "\"\\x1b[38;5;111m00 \","
   6     next
   7 }
   8 
   9 # ascii symbol which need backslashing
  10 $0 == 34 || $0 == 92 {
  11     printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m\\%c\",\n", $0 + 0, $0
  12     next
  13 }
  14 
  15 # all other ascii symbol
  16 32 <= $0 && $0 <= 126 {
  17     printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m%c\",\n", $0 + 0, $0
  18     next
  19 }
  20 
  21 # all 1 bits
  22 $0 == 255 {
  23     print "\"\\x1b[38;5;209mff \","
  24     next
  25 }
  26 
  27 # all other bytes
  28 1 {
  29     printf "\"\\x1b[38;5;246m%02x \",\n", $0 + 0
  30     next
  31 }

     File: ./hex-symbols.awk
   1 #!/usr/bin/awk -f
   2 
   3 # ascii symbol which need backslashing
   4 $0 == 34 || $0 == 92 {
   5     printf "\"%02x\\%c\",\n", $0 + 0, $0
   6     next
   7 }
   8 
   9 # all other ascii symbol
  10 32 <= $0 && $0 <= 126 {
  11     printf "\"%02x%c\",\n", $0 + 0, $0
  12     next
  13 }
  14 
  15 # all other bytes
  16 1 {
  17     printf "\"%02x \",\n", $0 + 0
  18     next
  19 }

     File: ./info.txt
   1 hex [options...] [filenames...]
   2 
   3 A simple hexadecimal viewer for easy byte-level inspection of files/data.
   4 
   5 Each line shows the starting offset for the bytes shown, 20 of the bytes
   6 themselves in base-16 notation, and any ASCII codes when the byte values
   7 are in the typical ASCII range.
   8 
   9 The base-16 codes are color-coded, with most bytes shown in gray, while
  10 all-1 and all-0 bytes are shown in orange and blue respectively.
  11 
  12 All-0 bytes are the commonest kind in most binary file types and, along
  13 with all-1 bytes are also a special case worth noticing when exploring
  14 binary data, so it makes sense for them to stand out right away.

     File: ./logo.ico   <BINARY>

     File: ./logo.png   <BINARY>

     File: ./main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "fmt"
   6     "io"
   7     "math"
   8     "os"
   9 
  10     _ "embed"
  11 )
  12 
  13 //go:embed info.txt
  14 var usage string
  15 
  16 func main() {
  17     err := run(parseFlags(usage))
  18     if err != nil {
  19         os.Stderr.WriteString(err.Error())
  20         os.Stderr.WriteString("\n")
  21         os.Exit(1)
  22     }
  23 }
  24 
  25 func run(cfg config) error {
  26     // f, _ := os.Create(`hex.prof`)
  27     // defer f.Close()
  28     // pprof.StartCPUProfile(f)
  29     // defer pprof.StopCPUProfile()
  30 
  31     w := bufio.NewWriterSize(os.Stdout, 64*1024)
  32     defer w.Flush()
  33 
  34     // with no filenames given, handle stdin and quit
  35     if len(cfg.Filenames) == 0 {
  36         return handle(w, os.Stdin, `<stdin>`, -1, cfg)
  37     }
  38 
  39     // show all files given
  40     for i, fname := range cfg.Filenames {
  41         if i > 0 {
  42             w.WriteString("\n")
  43             w.WriteString("\n")
  44         }
  45 
  46         err := handleFile(w, fname, cfg)
  47         if err != nil {
  48             return err
  49         }
  50     }
  51 
  52     return nil
  53 }
  54 
  55 // handleFile is like handleReader, except it also shows file-related info
  56 func handleFile(w *bufio.Writer, fname string, cfg config) error {
  57     f, err := os.Open(fname)
  58     if err != nil {
  59         return err
  60     }
  61     defer f.Close()
  62 
  63     stat, err := f.Stat()
  64     if err != nil {
  65         return handle(w, f, fname, -1, cfg)
  66     }
  67 
  68     fsize := int(stat.Size())
  69     return handle(w, f, fname, fsize, cfg)
  70 }
  71 
  72 // handle shows some messages related to the input and the cmd-line options
  73 // used, and then follows them by the hexadecimal byte-view
  74 func handle(w *bufio.Writer, r io.Reader, name string, size int, cfg config) error {
  75     skip(r, cfg.Skip)
  76     if cfg.MaxBytes > 0 {
  77         r = io.LimitReader(r, int64(cfg.MaxBytes))
  78     }
  79 
  80     // finish config setup based on the filesize, if a valid one was given
  81     if cfg.OffsetCounterWidth < 1 {
  82         if size < 1 {
  83             cfg.OffsetCounterWidth = defaultOffsetCounterWidth
  84         } else {
  85             w := math.Log10(float64(size))
  86             w = math.Max(math.Ceil(w), 1)
  87             cfg.OffsetCounterWidth = uint(w)
  88         }
  89     }
  90 
  91     switch cfg.To {
  92     case plainOutput:
  93         writeMetaPlain(w, name, size, cfg)
  94         // when done, emit a new line in case only part of the last line is
  95         // shown, which means no newline was emitted for it
  96         defer w.WriteString("\n")
  97         return render(w, r, cfg, writeBufferPlain)
  98 
  99     case ansiOutput:
 100         writeMetaANSI(w, name, size, cfg)
 101         // when done, emit a new line in case only part of the last line is
 102         // shown, which means no newline was emitted for it
 103         defer w.WriteString("\x1b[0m\n")
 104         return render(w, r, cfg, writeBufferANSI)
 105 
 106     default:
 107         const fs = `unsupported output format %q`
 108         return fmt.Errorf(fs, cfg.To)
 109     }
 110 }
 111 
 112 // skip ignores n bytes from the reader given
 113 func skip(r io.Reader, n int) {
 114     if n < 1 {
 115         return
 116     }
 117 
 118     // use func Seek for input files, except for stdin, which you can't seek
 119     if f, ok := r.(*os.File); ok && r != os.Stdin {
 120         f.Seek(int64(n), io.SeekCurrent)
 121         return
 122     }
 123     io.CopyN(io.Discard, r, int64(n))
 124 }
 125 
 126 // renderer is the type for the hex-view render funcs
 127 type renderer func(rc rendererConfig, first, second []byte) error
 128 
 129 // render reads all input and shows the hexadecimal byte-view for the input
 130 // data via the rendering callback given
 131 func render(w *bufio.Writer, r io.Reader, cfg config, fn renderer) error {
 132     if cfg.PerLine < 1 {
 133         cfg.PerLine = 16
 134     }
 135 
 136     rc := rendererConfig{
 137         out:     w,
 138         offset:  uint(cfg.Skip),
 139         chunks:  0,
 140         perLine: uint(cfg.PerLine),
 141         ruler:   cfg.Ruler,
 142 
 143         offsetWidth: cfg.OffsetCounterWidth,
 144         showOffsets: cfg.ShowOffsets,
 145         showASCII:   cfg.ShowASCII,
 146     }
 147 
 148     // calling func Read directly can sometimes result in chunks shorter
 149     // than the max chunk-size, even when there are plenty of bytes yet
 150     // to read; to avoid that, use a buffered-reader to explicitly fill
 151     // a slice instead
 152     br := bufio.NewReader(r)
 153 
 154     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 155     cur := make([]byte, 0, cfg.PerLine)
 156     ahead := make([]byte, 0, cfg.PerLine)
 157 
 158     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 159     // so to speak
 160     cur, err := fillChunk(cur[:0], cfg.PerLine, br)
 161     if len(cur) == 0 {
 162         if err == io.EOF {
 163             err = nil
 164         }
 165         return err
 166     }
 167 
 168     for {
 169         ahead, err := fillChunk(ahead[:0], cfg.PerLine, br)
 170         if err != nil && err != io.EOF {
 171             return err
 172         }
 173 
 174         if len(ahead) == 0 {
 175             // done, maybe except for an extra line of output
 176             break
 177         }
 178 
 179         // show the byte-chunk on its own output line
 180         err = fn(rc, cur, ahead)
 181         if err != nil {
 182             // probably a pipe was closed
 183             return nil
 184         }
 185 
 186         rc.chunks++
 187         rc.offset += uint(len(cur))
 188         cur = cur[:copy(cur, ahead)]
 189     }
 190 
 191     // don't forget the last output line
 192     if rc.chunks > 0 && len(cur) > 0 {
 193         return fn(rc, cur, nil)
 194     }
 195     return nil
 196 }
 197 
 198 // fillChunk tries to read the number of bytes given, appending them to the
 199 // byte-slice given; this func returns an EOF error only when no bytes are
 200 // read, which somewhat simplifies error-handling for the func caller
 201 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 202     // read buffered-bytes up to the max chunk-size
 203     for i := 0; i < n; i++ {
 204         b, err := br.ReadByte()
 205         if err == nil {
 206             chunk = append(chunk, b)
 207             continue
 208         }
 209 
 210         if err == io.EOF && i > 0 {
 211             return chunk, nil
 212         }
 213         return chunk, err
 214     }
 215 
 216     // got the full byte-count asked for
 217     return chunk, nil
 218 }

     File: ./numbers.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "io"
   6     "math"
   7     "strconv"
   8     "strings"
   9 )
  10 
  11 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
  12 // handles negatives, even though this app only uses it with non-negatives.
  13 func loopThousandsGroups(n int, fn func(i, n int)) {
  14     // 0 doesn't have a log10
  15     if n == 0 {
  16         fn(0, 0)
  17         return
  18     }
  19 
  20     sign := +1
  21     if n < 0 {
  22         n = -n
  23         sign = -1
  24     }
  25 
  26     intLog1000 := int(math.Log10(float64(n)) / 3)
  27     remBase := int(math.Pow10(3 * intLog1000))
  28 
  29     for i := 0; remBase > 0; i++ {
  30         group := (1000 * n) / remBase / 1000
  31         fn(i, sign*group)
  32         // if original number was negative, ensure only first
  33         // group gives a negative input to the callback
  34         sign = +1
  35 
  36         n %= remBase
  37         remBase /= 1000
  38     }
  39 }
  40 
  41 // sprintCommas turns the non-negative number given into a readable string,
  42 // where digits are grouped-separated by commas
  43 func sprintCommas(n int) string {
  44     var sb strings.Builder
  45     loopThousandsGroups(n, func(i, n int) {
  46         if i == 0 {
  47             var buf [4]byte
  48             sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
  49             return
  50         }
  51         sb.WriteByte(',')
  52         writePad0Sub1000Counter(&sb, uint(n))
  53     })
  54     return sb.String()
  55 }
  56 
  57 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
  58 func writePad0Sub1000Counter(w io.Writer, n uint) {
  59     // precondition is 0...999
  60     if n > 999 {
  61         w.Write([]byte(`???`))
  62         return
  63     }
  64 
  65     var buf [3]byte
  66     buf[0] = byte(n/100) + '0'
  67     n %= 100
  68     buf[1] = byte(n/10) + '0'
  69     buf[2] = byte(n%10) + '0'
  70     w.Write(buf[:])
  71 }
  72 
  73 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
  74 // matters because it's called for every byte of input which isn't
  75 // all 0s or all 1s
  76 func writeHex(w *bufio.Writer, b byte) {
  77     const hexDigits = `0123456789abcdef`
  78     w.WriteByte(hexDigits[b>>4])
  79     w.WriteByte(hexDigits[b&0x0f])
  80 }

     File: ./plain.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "fmt"
   6     "strconv"
   7 )
   8 
   9 // padding is the padding/spacing emitted across each output line, except for
  10 // the breather/ruler lines
  11 const padding = `  `
  12 
  13 // writeMetaPlain shows metadata right before the plain-text hex byte-view
  14 func writeMetaPlain(w *bufio.Writer, fname string, fsize int, cfg config) {
  15     if cfg.Title != `` {
  16         w.WriteString(cfg.Title)
  17         w.WriteString("\n")
  18         w.WriteString("\n")
  19     }
  20 
  21     if fsize < 0 {
  22         fmt.Fprintf(w, "• %s\n", fname)
  23     } else {
  24         const fs = "• %s  (%s bytes)\n"
  25         fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
  26     }
  27 
  28     if cfg.Skip > 0 {
  29         const fs = "   skipping first %s bytes\n"
  30         fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
  31     }
  32     if cfg.MaxBytes > 0 {
  33         const fs = "   showing only up to %s bytes\n"
  34         fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
  35     }
  36     w.WriteString("\n")
  37 }
  38 
  39 // writeBufferPlain shows the hex byte-view withOUT using ANSI colors/styles
  40 func writeBufferPlain(rc rendererConfig, first, second []byte) error {
  41     // show a ruler every few lines to make eye-scanning easier
  42     if rc.chunks%5 == 0 && rc.chunks > 0 {
  43         rc.out.WriteByte('\n')
  44     }
  45 
  46     return writeLinePlain(rc, first, second)
  47 }
  48 
  49 func writeLinePlain(rc rendererConfig, first, second []byte) error {
  50     w := rc.out
  51 
  52     // start each line with the byte-offset for the 1st item shown on it
  53     if rc.showOffsets {
  54         writePlainCounter(w, int(rc.offsetWidth), rc.offset)
  55         w.WriteByte(' ')
  56     } else {
  57         w.WriteString(padding)
  58     }
  59 
  60     for _, b := range first {
  61         // fmt.Fprintf(w, ` %02x`, b)
  62         //
  63         // the commented part above was a performance bottleneck, since
  64         // the slow/generic fmt.Fprintf was called for each input byte
  65         w.WriteByte(' ')
  66         writeHex(w, b)
  67     }
  68 
  69     if rc.showASCII {
  70         writePlainASCII(w, first, second, int(rc.perLine))
  71     }
  72 
  73     return w.WriteByte('\n')
  74 }
  75 
  76 // writePlainCounter just emits a left-padded number
  77 func writePlainCounter(w *bufio.Writer, width int, n uint) {
  78     var buf [32]byte
  79     str := strconv.AppendUint(buf[:0], uint64(n), 10)
  80     writeSpaces(w, width-len(str))
  81     w.Write(str)
  82 }
  83 
  84 // writeRulerPlain emits a breather line using a ruler-like pattern of spaces
  85 // and dots, to guide the eye across the main output lines
  86 // func writeRulerPlain(w *bufio.Writer, indent int, offset int, numitems int) {
  87 //  writeSpaces(w, indent)
  88 //  for i := 0; i < numitems-1; i++ {
  89 //      if (i+offset+1)%5 == 0 {
  90 //          w.WriteString(`   `)
  91 //      } else {
  92 //          w.WriteString(`  ·`)
  93 //      }
  94 //  }
  95 // }
  96 
  97 // writeSpaces bulk-emits the number of spaces given
  98 func writeSpaces(w *bufio.Writer, n int) {
  99     const spaces = `                                `
 100     for ; n > len(spaces); n -= len(spaces) {
 101         w.WriteString(spaces)
 102     }
 103     if n > 0 {
 104         w.WriteString(spaces[:n])
 105     }
 106 }
 107 
 108 // writePlainASCII emits the side-panel showing all ASCII runs for each line
 109 func writePlainASCII(w *bufio.Writer, first, second []byte, perline int) {
 110     // prev keeps track of the previous byte, so spaces are added
 111     // when bytes change from non-visible-ASCII to visible-ASCII
 112     prev := byte(0)
 113 
 114     spaces := 3*(perline-len(first)) + len(padding)
 115 
 116     for _, b := range first {
 117         if 32 < b && b < 127 {
 118             if !(32 < prev && prev < 127) {
 119                 writeSpaces(w, spaces)
 120                 spaces = 1
 121             }
 122             w.WriteByte(b)
 123         }
 124         prev = b
 125     }
 126 
 127     for _, b := range second {
 128         if 32 < b && b < 127 {
 129             if !(32 < prev && prev < 127) {
 130                 writeSpaces(w, spaces)
 131                 spaces = 1
 132             }
 133             w.WriteByte(b)
 134         }
 135         prev = b
 136     }
 137 }

     File: ./winapp.rc
   1 // https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
   2 // windres -o winapp_amd64.syso winapp.rc
   3 
   4 IDI_ICON1 ICON "logo.ico"

     File: ./winapp_amd64.syso   <BINARY>