File: chex/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;248m(%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;248m") 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;248m") 383 w.Write(str[:3]) 384 w.WriteString("\x1b[0m") 385 } 386 387 style = !style 388 str = str[3:] 389 } 390 } File: chex/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: chex/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: chex/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: chex/info.txt 1 chex [options...] [filenames...] 2 3 Colored HEX is a simple hexadecimal viewer for easy byte-level inspection 4 of files/data. 5 6 Each line shows the starting offset for the bytes shown, 16 of the bytes 7 themselves in base-16 notation, and any ASCII codes when the byte values 8 are in the typical ASCII range. The offsets shown are base-10. 9 10 The base-16 codes are color-coded, with most bytes shown in gray, while 11 all-1 and all-0 bytes are shown in orange and blue respectively. 12 13 All-0 bytes are the commonest kind in most binary file types and, along 14 with all-1 bytes are also a special case worth noticing when exploring 15 binary data, so it makes sense for them to stand out right away. File: chex/logo.ico <BINARY> File: chex/logo.png <BINARY> File: chex/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, 16*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: chex/mit-license.txt 1 The MIT License (MIT) 2 3 Copyright © 2024 pacman64 4 5 Permission is hereby granted, free of charge, to any person obtaining a copy of 6 this software and associated documentation files (the “Software”), to deal 7 in the Software without restriction, including without limitation the rights to 8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 of the Software, and to permit persons to whom the Software is furnished to do 10 so, subject to the following conditions: 11 12 The above copyright notice and this permission notice shall be included in all 13 copies or substantial portions of the Software. 14 15 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 SOFTWARE. File: chex/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: chex/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: chex/winapp_amd64.syso <BINARY> File: chex/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"