[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

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

ref(wcwidth): deslop

Nate Moore (Jul 2, 2026, 12:16 AM EDT) bcaeb0aa 58d347ce

+42 -70
+42 -70
tasks/gen-wcwidth.ts
··· 1 1 // gen-wcwidth.ts — generate src/wcwidth.c from Unicode 16.0 data 2 2 // Usage: deno task gen-wcwidth 3 3 // 4 - // wcwidth = "wide character width", a POSIX standard function. 5 - // This script fetches three Unicode data files and emits a compact C 6 - // implementation with two lookup strategies: 7 - // 8 - // "packed" ranges — uint32_t where bits 31–8 = start codepoint, 9 - // bits 7–0 = count (max 255). Used for combining 10 - // marks and small wide ranges. 11 - // 12 - // "large" ranges — separate uint32_t starts[] + uint16_t counts[] 13 - // for the handful of CJK/Hangul blocks whose range 14 - // spans more than 255 codepoints. 4 + // Packed encoding (*_small_ranges): each uint32_t packs start codepoint in 5 + // bits 31–8 and count in bits 7–0. Large ranges (count > 255) use separate 6 + // starts[] + counts[] arrays. 15 7 16 8 const UNICODE_BASE = "https://www.unicode.org/Public/16.0.0/ucd"; 17 9 18 - // --------------------------------------------------------------------------- 19 - // Types 20 - // --------------------------------------------------------------------------- 21 - 22 10 interface Interval { 23 11 start: number; 24 12 end: number; // inclusive 25 13 } 26 14 27 - // --------------------------------------------------------------------------- 28 - // Fetch 29 - // --------------------------------------------------------------------------- 30 - 31 15 async function fetchText(path: string): Promise<string> { 32 16 const url = `${UNICODE_BASE}/${path}`; 33 17 console.error(`Fetching ${url} …`); ··· 38 22 return response.text(); 39 23 } 40 24 41 - // --------------------------------------------------------------------------- 42 - // Parse 43 - // --------------------------------------------------------------------------- 44 - 45 25 function parseCodepointRange(token: string): Interval { 46 26 if (token.includes("..")) { 47 27 const [lo, hi] = token.split("..").map((s) => parseInt(s, 16)); ··· 51 31 return { start: cp, end: cp }; 52 32 } 53 33 54 - // extracted/DerivedGeneralCategory.txt → "XXXX..YYYY ; Category # …" 34 + /** 35 + * Parses `extracted/DerivedGeneralCategory.txt` lines of the form 36 + * `XXXX..YYYY ; Category # …` and returns intervals matching `category`. 37 + */ 55 38 function parseGeneralCategory(text: string, category: string): Interval[] { 56 39 const intervals: Interval[] = []; 57 40 for (const line of text.split("\n")) { ··· 64 47 return intervals; 65 48 } 66 49 67 - // DerivedCoreProperties.txt → "XXXX ; Property_Name # …" 50 + /** 51 + * Parses `DerivedCoreProperties.txt` lines of the form 52 + * `XXXX ; Property_Name # …` and returns intervals matching `property`. 53 + */ 68 54 function parseDerivedProperty(text: string, property: string): Interval[] { 69 55 const intervals: Interval[] = []; 70 56 for (const line of text.split("\n")) { ··· 77 63 return intervals; 78 64 } 79 65 80 - // EastAsianWidth.txt → "XXXX ; W|F # …" (W = Wide, F = Fullwidth) 66 + /** 67 + * Parses `EastAsianWidth.txt` lines of the form `XXXX ; W|F # …` 68 + * and returns intervals for Wide (W) and Fullwidth (F) codepoints. 69 + */ 81 70 function parseWideEastAsian(text: string): Interval[] { 82 71 const intervals: Interval[] = []; 83 72 for (const line of text.split("\n")) { ··· 90 79 return intervals; 91 80 } 92 81 93 - // --------------------------------------------------------------------------- 94 - // Range helpers 95 - // --------------------------------------------------------------------------- 96 - 97 82 function mergeIntervals(intervals: Interval[]): Interval[] { 98 83 if (intervals.length === 0) return []; 99 84 intervals.sort((a, b) => a.start - b.start); ··· 117 102 if (current.start <= previous.end + 1) { 118 103 throw new Error( 119 104 `${label}: adjacent ranges at index ${index}: ` + 120 - `[0x${previous.start.toString(16)}, 0x${previous.end.toString(16)}] ` + 121 - `and [0x${current.start.toString(16)}, 0x${current.end.toString(16)}]`, 105 + `[0x${previous.start.toString(16)}, 0x${ 106 + previous.end.toString(16) 107 + }] ` + 108 + `and [0x${current.start.toString(16)}, 0x${ 109 + current.end.toString(16) 110 + }]`, 122 111 ); 123 112 } 124 113 } 125 114 } 126 115 127 - // --------------------------------------------------------------------------- 128 - // C emit helpers 129 - // --------------------------------------------------------------------------- 130 - 131 116 function formatUint32Hex(value: number): string { 132 117 return `0x${(value >>> 0).toString(16).padStart(8, "0")}`; 133 118 } ··· 154 139 return lines.join("\n"); 155 140 } 156 141 157 - // --------------------------------------------------------------------------- 158 - // Main 159 - // --------------------------------------------------------------------------- 160 - 161 142 const [derivedCategoryText, derivedCorePropsText, eastAsianWidthText] = 162 143 await Promise.all([ 163 144 fetchText("extracted/DerivedGeneralCategory.txt"), 164 145 fetchText("DerivedCoreProperties.txt"), 165 146 fetchText("EastAsianWidth.txt"), 166 147 ]); 167 - 168 - // --- Collect width-0 intervals --- 169 148 170 149 const nonspacingMarks = parseGeneralCategory(derivedCategoryText, "Mn"); 171 150 const enclosingMarks = parseGeneralCategory(derivedCategoryText, "Me"); ··· 174 153 "Default_Ignorable_Code_Point", 175 154 ); 176 155 177 - // Merge all zero-width sources, then strip anything in the fast-path range 178 - // (0x00–0xFF). Those codepoints are handled by hard-coded checks in wcwidth() 179 - // so there is no point carrying them in the lookup table. 156 + // 0x00–0xFF is handled by fast-path checks in wcwidth(), so strip it from the table. 180 157 const allZeroWidth = mergeIntervals([ 181 158 ...nonspacingMarks, 182 159 ...enclosingMarks, ··· 185 162 const combiningIntervals = mergeIntervals( 186 163 allZeroWidth 187 164 .filter((interval) => interval.end > 0xff) 188 - .map((interval) => ({ start: Math.max(interval.start, 0x100), end: interval.end })), 165 + .map((interval) => ({ 166 + start: Math.max(interval.start, 0x100), 167 + end: interval.end, 168 + })), 189 169 ); 190 170 assertNoAdjacentRanges(combiningIntervals, "combining"); 191 171 192 - // --- Collect width-2 intervals --- 193 - 194 172 const wideIntervals = mergeIntervals(parseWideEastAsian(eastAsianWidthText)); 195 173 assertNoAdjacentRanges(wideIntervals, "wide"); 196 - 197 - // --- Convert intervals to (start, count) pairs --- 198 174 199 175 const combiningRanges = combiningIntervals.map((interval) => ({ 200 176 start: interval.start, ··· 206 182 count: interval.end - interval.start, 207 183 })); 208 184 209 - // --- Assert 24-bit start constraint (hard limit: Unicode fits in 21 bits) --- 210 - 211 185 for (const range of [...combiningRanges, ...wideRanges]) { 212 186 if (range.start > 0xffffff) { 213 187 throw new Error( 214 - `Range start 0x${range.start.toString(16)} exceeds 24 bits — packed encoding broken`, 188 + `Range start 0x${ 189 + range.start.toString(16) 190 + } exceeds 24 bits — packed encoding broken`, 215 191 ); 216 192 } 217 193 } 218 194 219 - // --- Split both combining and wide ranges into small (count ≤ 255) / large --- 220 - // 221 195 // Unicode 16.0's DerivedCoreProperties.txt includes E0000..E0FFF as a single 222 196 // Default_Ignorable block (count = 4095), so combining ranges also need the 223 197 // small/large split — not just wide ranges. 224 - 225 - const combiningSmallRanges = combiningRanges.filter((range) => range.count <= 255); 226 - const combiningLargeRanges = combiningRanges.filter((range) => range.count > 255); 198 + const combiningSmallRanges = combiningRanges.filter((range) => 199 + range.count <= 255 200 + ); 201 + const combiningLargeRanges = combiningRanges.filter((range) => 202 + range.count > 255 203 + ); 227 204 const wideSmallRanges = wideRanges.filter((range) => range.count <= 255); 228 205 const wideLargeRanges = wideRanges.filter((range) => range.count > 255); 229 - 230 - // --- Pack small entries: (start << 8) | count --- 231 206 232 207 const combiningSmallPacked = combiningSmallRanges.map( 233 208 (range) => (range.start << 8) | range.count, ··· 240 215 const wideLargeStarts = wideLargeRanges.map((range) => range.start); 241 216 const wideLargeCounts = wideLargeRanges.map((range) => range.count); 242 217 243 - // --- Verify sort invariants (packed arrays must be strictly increasing) --- 244 - 245 218 for (let index = 1; index < combiningSmallPacked.length; index++) { 246 219 if (combiningSmallPacked[index] <= combiningSmallPacked[index - 1]) { 247 - throw new Error(`combining_small_ranges not strictly increasing at index ${index}`); 220 + throw new Error( 221 + `combining_small_ranges not strictly increasing at index ${index}`, 222 + ); 248 223 } 249 224 } 250 225 for (let index = 1; index < wideSmallPacked.length; index++) { 251 226 if (wideSmallPacked[index] <= wideSmallPacked[index - 1]) { 252 - throw new Error(`wide_small_ranges not strictly increasing at index ${index}`); 227 + throw new Error( 228 + `wide_small_ranges not strictly increasing at index ${index}`, 229 + ); 253 230 } 254 231 } 255 232 256 - // --- Report --- 257 - 258 - const tableBytes = 259 - combiningSmallPacked.length * 4 + 233 + const tableBytes = combiningSmallPacked.length * 4 + 260 234 combiningLargeStarts.length * 4 + 261 235 combiningLargeCounts.length * 2 + 262 236 wideSmallPacked.length * 4 + ··· 268 242 console.error(`wide_small_ranges: ${wideSmallPacked.length} entries`); 269 243 console.error(`wide_large_ranges: ${wideLargeRanges.length} entries`); 270 244 console.error(`Table data: ${tableBytes} bytes`); 271 - 272 - // --- Emit src/wcwidth.c --- 273 245 274 246 const date = new Date().toISOString().slice(0, 10); 275 247