Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix: text wrapping in blank.mjs, accurate TextButton width measurement, vscode ext v1.271.0

- Use write max-width param for centered multi-line description in blank.mjs
- Fix TextButton width calculation to use per-character advance for variable-width typefaces
- Bump vscode extension to 1.271.0
- Add publish-changelog and publish-commits scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+662 -8
+398
at/publish-changelog.mjs
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * Publish a changelog post to change.pckt.blog 5 + * 6 + * Creates a site.standard.document record in the aesthetic.computer PDS 7 + * using the blog.pckt.content block format. 8 + * 9 + * Usage: 10 + * node at/publish-changelog.mjs "Title" "Body text in markdown-ish format" 11 + * node at/publish-changelog.mjs --from-commits 5 # last 5 commits 12 + * node at/publish-changelog.mjs --file changelog.md # from a file 13 + * echo "content" | node at/publish-changelog.mjs "Title" --stdin 14 + * 15 + * Manage posts: 16 + * node at/publish-changelog.mjs --list 17 + * node at/publish-changelog.mjs --delete <rkey> 18 + * 19 + * Body supports a simple subset: 20 + * ## Heading → blog.pckt.block.heading (level 2) 21 + * ### Heading → blog.pckt.block.heading (level 3) 22 + * **bold text** → blog.pckt.richtext.facet#bold 23 + * *italic text* → blog.pckt.richtext.facet#italic 24 + * [text](url) → blog.pckt.richtext.facet#link 25 + * blank line → paragraph break 26 + */ 27 + 28 + import { AtpAgent } from "@atproto/api"; 29 + import { config } from "dotenv"; 30 + import { readFileSync } from "fs"; 31 + import { execSync } from "child_process"; 32 + 33 + config({ path: new URL(".env", import.meta.url) }); 34 + 35 + const BSKY_SERVICE = process.env.BSKY_SERVICE || "https://bsky.social"; 36 + const BSKY_IDENTIFIER = process.env.BSKY_IDENTIFIER; 37 + const BSKY_APP_PASSWORD = process.env.BSKY_APP_PASSWORD; 38 + 39 + // The publication record URI for change.pckt.blog 40 + const PUBLICATION_URI = 41 + "at://did:plc:k3k3wknzkcnekbnyde4dbatz/site.standard.publication/3mhrqpf4rqykh"; 42 + 43 + // --------------------------------------------------------------------------- 44 + // Markdown-ish → pckt blocks 45 + // --------------------------------------------------------------------------- 46 + 47 + function parseInline(text) { 48 + // Extract bold, italic, and link facets from a line of text. 49 + // Returns { plaintext, facets } 50 + const facets = []; 51 + let plain = ""; 52 + 53 + // Regex order matters — bold before italic (** before *) 54 + // We process left-to-right, replacing markup and tracking byte offsets. 55 + const tokens = 56 + text.match(/\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|[^*[\]]+/g) || []; 57 + 58 + for (const tok of tokens) { 59 + const byteStart = Buffer.byteLength(plain, "utf8"); 60 + 61 + if (tok.startsWith("**") && tok.endsWith("**")) { 62 + const inner = tok.slice(2, -2); 63 + plain += inner; 64 + facets.push({ 65 + index: { 66 + byteStart, 67 + byteEnd: byteStart + Buffer.byteLength(inner, "utf8"), 68 + }, 69 + features: [{ $type: "blog.pckt.richtext.facet#bold" }], 70 + }); 71 + } else if (tok.startsWith("*") && tok.endsWith("*")) { 72 + const inner = tok.slice(1, -1); 73 + plain += inner; 74 + facets.push({ 75 + index: { 76 + byteStart, 77 + byteEnd: byteStart + Buffer.byteLength(inner, "utf8"), 78 + }, 79 + features: [{ $type: "blog.pckt.richtext.facet#italic" }], 80 + }); 81 + } else if (tok.startsWith("[")) { 82 + const m = tok.match(/^\[([^\]]+)\]\(([^)]+)\)$/); 83 + if (m) { 84 + const [, linkText, url] = m; 85 + plain += linkText; 86 + facets.push({ 87 + index: { 88 + byteStart, 89 + byteEnd: byteStart + Buffer.byteLength(linkText, "utf8"), 90 + }, 91 + features: [{ $type: "blog.pckt.richtext.facet#link", uri: url }], 92 + }); 93 + } else { 94 + plain += tok; 95 + } 96 + } else { 97 + plain += tok; 98 + } 99 + } 100 + 101 + return { plaintext: plain, facets }; 102 + } 103 + 104 + function markdownToBlocks(md) { 105 + const lines = md.split("\n"); 106 + const items = []; 107 + let paragraph = []; 108 + 109 + function flushParagraph() { 110 + if (paragraph.length === 0) return; 111 + const text = paragraph.join(" ").trim(); 112 + paragraph = []; 113 + if (!text) return; 114 + 115 + const { plaintext, facets } = parseInline(text); 116 + const block = { $type: "blog.pckt.block.text", plaintext }; 117 + if (facets.length > 0) block.facets = facets; 118 + items.push(block); 119 + } 120 + 121 + for (const line of lines) { 122 + const trimmed = line.trim(); 123 + 124 + // Heading 125 + const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/); 126 + if (headingMatch) { 127 + flushParagraph(); 128 + const level = headingMatch[1].length; 129 + items.push({ 130 + $type: "blog.pckt.block.heading", 131 + level, 132 + plaintext: headingMatch[2], 133 + }); 134 + continue; 135 + } 136 + 137 + // Blank line = paragraph break 138 + if (trimmed === "") { 139 + flushParagraph(); 140 + continue; 141 + } 142 + 143 + // Bullet points — prefix with bullet, treat as text 144 + if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) { 145 + flushParagraph(); 146 + const bulletText = "\u2022 " + trimmed.slice(2); 147 + const { plaintext, facets } = parseInline(bulletText); 148 + const block = { $type: "blog.pckt.block.text", plaintext }; 149 + if (facets.length > 0) block.facets = facets; 150 + items.push(block); 151 + continue; 152 + } 153 + 154 + paragraph.push(trimmed); 155 + } 156 + 157 + flushParagraph(); 158 + return items; 159 + } 160 + 161 + // --------------------------------------------------------------------------- 162 + // Generate changelog from git commits 163 + // --------------------------------------------------------------------------- 164 + 165 + function changelogFromCommits(count = 10) { 166 + const log = execSync( 167 + `git log --oneline --no-decorate -n ${count} --format="%h %s"`, 168 + { encoding: "utf8", cwd: process.env.INIT_CWD || process.cwd() }, 169 + ).trim(); 170 + 171 + const lines = log.split("\n"); 172 + const date = new Date().toISOString().slice(0, 10); 173 + let md = `## Changelog ${date}\n\n`; 174 + for (const line of lines) { 175 + md += `- ${line}\n`; 176 + } 177 + return md; 178 + } 179 + 180 + // --------------------------------------------------------------------------- 181 + // Slug generation 182 + // --------------------------------------------------------------------------- 183 + 184 + function slugify(title) { 185 + const slug = title 186 + .toLowerCase() 187 + .replace(/[^a-z0-9]+/g, "-") 188 + .replace(/^-|-$/g, ""); 189 + // Append short random suffix like pckt does 190 + const rand = Math.random().toString(36).slice(2, 9); 191 + return `/${slug}-${rand}`; 192 + } 193 + 194 + // --------------------------------------------------------------------------- 195 + // Auth helper 196 + // --------------------------------------------------------------------------- 197 + 198 + async function login() { 199 + if (!BSKY_IDENTIFIER || !BSKY_APP_PASSWORD) { 200 + console.error("Missing BSKY_IDENTIFIER or BSKY_APP_PASSWORD in at/.env"); 201 + process.exit(1); 202 + } 203 + const agent = new AtpAgent({ service: BSKY_SERVICE }); 204 + await agent.login({ 205 + identifier: BSKY_IDENTIFIER, 206 + password: BSKY_APP_PASSWORD, 207 + }); 208 + return agent; 209 + } 210 + 211 + // --------------------------------------------------------------------------- 212 + // List posts 213 + // --------------------------------------------------------------------------- 214 + 215 + async function listPosts() { 216 + const agent = await login(); 217 + let cursor; 218 + const all = []; 219 + 220 + do { 221 + const res = await agent.com.atproto.repo.listRecords({ 222 + repo: agent.session.did, 223 + collection: "site.standard.document", 224 + limit: 100, 225 + cursor, 226 + }); 227 + // Only include posts belonging to our change publication 228 + for (const rec of res.data.records) { 229 + if (rec.value.site === PUBLICATION_URI) all.push(rec); 230 + } 231 + cursor = res.data.cursor; 232 + } while (cursor); 233 + 234 + if (all.length === 0) { 235 + console.log("No posts on change.pckt.blog yet."); 236 + return; 237 + } 238 + 239 + console.log(`\nchange.pckt.blog — ${all.length} post(s)\n`); 240 + for (const rec of all) { 241 + const rkey = rec.uri.split("/").pop(); 242 + const date = rec.value.publishedAt?.slice(0, 10) || "?"; 243 + console.log(` ${date} ${rkey} ${rec.value.title}`); 244 + } 245 + console.log(`\nTo delete: node at/publish-changelog.mjs --delete <rkey>`); 246 + } 247 + 248 + // --------------------------------------------------------------------------- 249 + // Delete a post 250 + // --------------------------------------------------------------------------- 251 + 252 + async function deletePost(rkey) { 253 + const agent = await login(); 254 + 255 + // Verify it exists and belongs to our publication 256 + try { 257 + const rec = await agent.com.atproto.repo.getRecord({ 258 + repo: agent.session.did, 259 + collection: "site.standard.document", 260 + rkey, 261 + }); 262 + if (rec.data.value.site !== PUBLICATION_URI) { 263 + console.error("That record doesn't belong to change.pckt.blog."); 264 + process.exit(1); 265 + } 266 + console.log(`Deleting: "${rec.data.value.title}" (${rkey})`); 267 + } catch { 268 + console.error(`Record not found: ${rkey}`); 269 + process.exit(1); 270 + } 271 + 272 + await agent.com.atproto.repo.deleteRecord({ 273 + repo: agent.session.did, 274 + collection: "site.standard.document", 275 + rkey, 276 + }); 277 + 278 + console.log("Deleted."); 279 + } 280 + 281 + // --------------------------------------------------------------------------- 282 + // Main 283 + // --------------------------------------------------------------------------- 284 + 285 + async function main() { 286 + const args = process.argv.slice(2); 287 + 288 + // Parse flags 289 + const flagIdx = (f) => args.indexOf(f); 290 + 291 + if (flagIdx("--list") !== -1) { 292 + return listPosts(); 293 + } 294 + 295 + if (flagIdx("--delete") !== -1) { 296 + const rkey = args[flagIdx("--delete") + 1]; 297 + if (!rkey) { 298 + console.error("Usage: --delete <rkey> (use --list to find rkeys)"); 299 + process.exit(1); 300 + } 301 + return deletePost(rkey); 302 + } 303 + 304 + if (!BSKY_IDENTIFIER || !BSKY_APP_PASSWORD) { 305 + console.error("Missing BSKY_IDENTIFIER or BSKY_APP_PASSWORD in at/.env"); 306 + process.exit(1); 307 + } 308 + 309 + let title, body; 310 + 311 + if (flagIdx("--from-commits") !== -1) { 312 + const n = parseInt(args[flagIdx("--from-commits") + 1]) || 10; 313 + title = args[0] && !args[0].startsWith("--") ? args[0] : `Changelog`; 314 + body = changelogFromCommits(n); 315 + } else if (flagIdx("--file") !== -1) { 316 + const filePath = args[flagIdx("--file") + 1]; 317 + const content = readFileSync(filePath, "utf8"); 318 + // First line starting with # is the title, rest is body 319 + const lines = content.split("\n"); 320 + const titleLine = lines.findIndex((l) => l.startsWith("# ")); 321 + if (titleLine !== -1) { 322 + title = lines[titleLine].replace(/^#+\s*/, ""); 323 + body = lines 324 + .slice(titleLine + 1) 325 + .join("\n") 326 + .trim(); 327 + } else { 328 + title = args[0] || "Update"; 329 + body = content; 330 + } 331 + } else if (flagIdx("--stdin") !== -1) { 332 + title = args[0] || "Update"; 333 + body = readFileSync("/dev/stdin", "utf8"); 334 + } else { 335 + title = args[0]; 336 + body = args[1]; 337 + if (!title) { 338 + console.error( 339 + `Usage: 340 + node at/publish-changelog.mjs "Title" "Body text" 341 + node at/publish-changelog.mjs --from-commits 5 342 + node at/publish-changelog.mjs --file changelog.md 343 + echo "text" | node at/publish-changelog.mjs "Title" --stdin 344 + node at/publish-changelog.mjs --list 345 + node at/publish-changelog.mjs --delete <rkey>`, 346 + ); 347 + process.exit(1); 348 + } 349 + if (!body) { 350 + body = ""; 351 + } 352 + } 353 + 354 + const items = markdownToBlocks(body); 355 + const path = slugify(title); 356 + const now = new Date().toISOString(); 357 + 358 + console.log(`Publishing to change.pckt.blog...`); 359 + console.log(` Title: ${title}`); 360 + console.log(` Path: ${path}`); 361 + console.log(` Blocks: ${items.length}`); 362 + 363 + const agent = await login(); 364 + console.log(` Authenticated as @${BSKY_IDENTIFIER}`); 365 + 366 + const record = { 367 + $type: "site.standard.document", 368 + site: PUBLICATION_URI, 369 + title, 370 + path, 371 + publishedAt: now, 372 + content: { 373 + $type: "blog.pckt.content", 374 + items, 375 + }, 376 + textContent: items 377 + .map((b) => b.plaintext || "") 378 + .filter(Boolean) 379 + .join("\n\n"), 380 + tags: ["changelog"], 381 + }; 382 + 383 + const res = await agent.com.atproto.repo.createRecord({ 384 + repo: agent.session.did, 385 + collection: "site.standard.document", 386 + record, 387 + }); 388 + 389 + console.log(`\nPublished!`); 390 + console.log(` URI: ${res.data.uri}`); 391 + console.log(` CID: ${res.data.cid}`); 392 + console.log(` URL: https://change.pckt.blog${path}`); 393 + } 394 + 395 + main().catch((err) => { 396 + console.error("Failed:", err.message); 397 + process.exit(1); 398 + });
+246
at/publish-commits.mjs
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * Publish a commit summary to news.aesthetic.computer 5 + * 6 + * Posts a "Commits From <start> to <end>" news item with a formatted 7 + * summary of git commits. Connects directly to MongoDB and optionally 8 + * syncs to ATProto PDS. 9 + * 10 + * Usage: 11 + * node at/publish-commits.mjs # last 20 commits 12 + * node at/publish-commits.mjs --count 50 # last 50 commits 13 + * node at/publish-commits.mjs --from abc1234 # from a specific hash 14 + * node at/publish-commits.mjs --from abc1234 --to def5678 15 + * node at/publish-commits.mjs --since "3 days ago" # git log --since 16 + * node at/publish-commits.mjs --dry-run # preview without posting 17 + */ 18 + 19 + import { MongoClient } from "mongodb"; 20 + import { AtpAgent } from "@atproto/api"; 21 + import { config } from "dotenv"; 22 + import { execSync } from "child_process"; 23 + import { randomBytes } from "crypto"; 24 + 25 + // Load env from multiple sources 26 + config({ path: new URL("../.devcontainer/envs/devcontainer.env", import.meta.url) }); 27 + config({ path: new URL(".env", import.meta.url) }); 28 + 29 + const MONGODB_URI = process.env.MONGODB_CONNECTION_STRING; 30 + const MONGODB_NAME = process.env.MONGODB_NAME || "aesthetic"; 31 + const ADMIN_SUB = process.env.ADMIN_SUB; 32 + const PDS_URL = process.env.PDS_URL || "https://at.aesthetic.computer"; 33 + 34 + // --------------------------------------------------------------------------- 35 + // Args 36 + // --------------------------------------------------------------------------- 37 + 38 + function parseArgs(argv) { 39 + const out = { _: [] }; 40 + for (let i = 0; i < argv.length; i++) { 41 + const t = argv[i]; 42 + if (!t.startsWith("--")) { out._.push(t); continue; } 43 + const key = t.slice(2); 44 + const next = argv[i + 1]; 45 + if (next && !next.startsWith("--")) { out[key] = next; i++; } 46 + else out[key] = true; 47 + } 48 + return out; 49 + } 50 + 51 + // --------------------------------------------------------------------------- 52 + // Git log 53 + // --------------------------------------------------------------------------- 54 + 55 + function getCommits(args) { 56 + const gitArgs = ["git", "log", "--oneline", "--no-decorate"]; 57 + 58 + if (args.from && args.to) { 59 + gitArgs.push(`${args.from}..${args.to}`); 60 + } else if (args.from) { 61 + gitArgs.push(`${args.from}..HEAD`); 62 + } else if (args.since) { 63 + gitArgs.push(`--since="${args.since}"`); 64 + } else { 65 + gitArgs.push(`-n`, `${args.count || 20}`); 66 + } 67 + 68 + gitArgs.push('--format="%h %s"'); 69 + 70 + const log = execSync(gitArgs.join(" "), { encoding: "utf8" }).trim(); 71 + if (!log) return { lines: [], first: null, last: null }; 72 + 73 + const lines = log.split("\n"); 74 + const first = lines[lines.length - 1].split(" ")[0]; // oldest 75 + const last = lines[0].split(" ")[0]; // newest 76 + return { lines, first, last }; 77 + } 78 + 79 + // --------------------------------------------------------------------------- 80 + // Format 81 + // --------------------------------------------------------------------------- 82 + 83 + function formatBody(lines) { 84 + return lines.map((l) => { 85 + const space = l.indexOf(" "); 86 + const hash = l.slice(0, space); 87 + const msg = l.slice(space + 1); 88 + return `${hash} ${msg}`; 89 + }).join("\n"); 90 + } 91 + 92 + // --------------------------------------------------------------------------- 93 + // Short code (same logic as generate-short-code.mjs) 94 + // --------------------------------------------------------------------------- 95 + 96 + const ALPHABET = "bcdfghjklmnpqrstvwxyzaeiou23456789"; 97 + 98 + function randomCode(len = 3) { 99 + const bytes = randomBytes(len); 100 + return Array.from(bytes).map((b) => ALPHABET[b % ALPHABET.length]).join(""); 101 + } 102 + 103 + async function uniqueCode(collection) { 104 + for (let i = 0; i < 100; i++) { 105 + const code = `n${randomCode()}`; 106 + const exists = await collection.findOne({ code }); 107 + if (!exists) return code; 108 + } 109 + throw new Error("Could not generate unique code after 100 attempts"); 110 + } 111 + 112 + // --------------------------------------------------------------------------- 113 + // ATProto sync 114 + // --------------------------------------------------------------------------- 115 + 116 + async function syncToAtproto(db, sub, newsData, refId) { 117 + const users = db.collection("users"); 118 + const user = await users.findOne({ _id: sub }); 119 + 120 + if (!user?.atproto?.did || !user?.atproto?.password) { 121 + console.log(" No ATProto account for user, skipping PDS sync."); 122 + return null; 123 + } 124 + 125 + const agent = new AtpAgent({ service: PDS_URL }); 126 + await agent.login({ 127 + identifier: user.atproto.did, 128 + password: user.atproto.password, 129 + }); 130 + 131 + const record = { 132 + $type: "computer.aesthetic.news", 133 + headline: newsData.headline, 134 + when: newsData.when.toISOString(), 135 + ref: refId, 136 + }; 137 + if (newsData.body) record.body = newsData.body; 138 + 139 + const res = await agent.com.atproto.repo.createRecord({ 140 + repo: user.atproto.did, 141 + collection: "computer.aesthetic.news", 142 + record, 143 + }); 144 + 145 + return { 146 + rkey: res.data.uri.split("/").pop(), 147 + uri: res.data.uri, 148 + did: user.atproto.did, 149 + }; 150 + } 151 + 152 + // --------------------------------------------------------------------------- 153 + // Main 154 + // --------------------------------------------------------------------------- 155 + 156 + async function main() { 157 + const args = parseArgs(process.argv.slice(2)); 158 + const dryRun = !!args["dry-run"]; 159 + 160 + const { lines, first, last } = getCommits(args); 161 + 162 + if (lines.length === 0) { 163 + console.error("No commits found."); 164 + process.exit(1); 165 + } 166 + 167 + const title = `Commits From ${first} to ${last}`; 168 + const body = formatBody(lines); 169 + 170 + console.log(`\n${title}`); 171 + console.log(`${lines.length} commit(s)\n`); 172 + console.log(body); 173 + 174 + if (dryRun) { 175 + console.log("\n--dry-run: not posting."); 176 + return; 177 + } 178 + 179 + if (!MONGODB_URI) { 180 + console.error("MONGODB_CONNECTION_STRING not set."); 181 + process.exit(1); 182 + } 183 + if (!ADMIN_SUB) { 184 + console.error("ADMIN_SUB not set."); 185 + process.exit(1); 186 + } 187 + 188 + const client = new MongoClient(MONGODB_URI); 189 + try { 190 + await client.connect(); 191 + const db = client.db(MONGODB_NAME); 192 + const posts = db.collection("news-posts"); 193 + const votes = db.collection("news-votes"); 194 + 195 + const code = await uniqueCode(posts); 196 + const now = new Date(); 197 + 198 + const doc = { 199 + code, 200 + title, 201 + url: "", 202 + text: body, 203 + user: ADMIN_SUB, 204 + when: now, 205 + updated: now, 206 + score: 1, 207 + commentCount: 0, 208 + status: "live", 209 + }; 210 + 211 + await posts.insertOne(doc); 212 + await votes.insertOne({ 213 + itemType: "post", 214 + itemId: code, 215 + user: ADMIN_SUB, 216 + when: now, 217 + }); 218 + 219 + console.log(`\nPosted to news.aesthetic.computer/${code}`); 220 + 221 + // ATProto sync 222 + try { 223 + const atproto = await syncToAtproto( 224 + db, 225 + ADMIN_SUB, 226 + { headline: title, body, when: now }, 227 + doc._id?.toString(), 228 + ); 229 + if (atproto) { 230 + await posts.updateOne({ code }, { $set: { atproto } }); 231 + console.log(` ATProto synced: ${atproto.uri}`); 232 + } 233 + } catch (e) { 234 + console.log(` ATProto sync failed: ${e.message}`); 235 + } 236 + 237 + console.log(` URL: https://news.aesthetic.computer/${code}`); 238 + } finally { 239 + await client.close(); 240 + } 241 + } 242 + 243 + main().catch((err) => { 244 + console.error("Failed:", err.message); 245 + process.exit(1); 246 + });
+1 -1
system/public/aesthetic.computer/disks/blank.mjs
··· 215 215 216 216 // Title + product description (centered, below HUD label) 217 217 ink(fg).write("AC Blank", { center: "x", y: 24, size: 2, screen }); 218 - ink(fgDim).write(DESCRIPTION, { center: "x", y: 48, width: floor(w * 0.8), screen }); 218 + ink(fgDim).write(DESCRIPTION, { center: "x", y: 48, screen }, undefined, floor(w * 0.85)); 219 219 220 220 // Thanks page 221 221 if (thanks) {
+14 -4
system/public/aesthetic.computer/lib/ui.mjs
··· 854 854 #g2 = this.#gap * 2; 855 855 #h = 12 + this.#g2; // 19; // 856 856 #offset = { x: this.#gap, y: this.#gap }; 857 + #typeface = null; 857 858 858 859 constructor(text = "Button", pos = { x: 0, y: 0 }, typeface = TYPEFACE_UI, gap = null) { 859 860 // Allow custom gap/padding if provided ··· 863 864 this.#offset = { x: this.#gap, y: this.#gap }; 864 865 } 865 866 867 + this.#typeface = typeface; 866 868 this.#cw = typeface.blockWidth; 867 869 this.#h = typeface.blockHeight + this.#gap * 2; 868 870 ··· 906 908 this.btn.stickyScrubbing = value; 907 909 } 908 910 911 + #measureTextWidth(text) { 912 + const visibleText = stripColorCodes(text); 913 + if (this.#typeface?.getAdvance) { 914 + let w = 0; 915 + for (const ch of visibleText) w += this.#typeface.getAdvance(ch); 916 + return w; 917 + } 918 + return visibleText.length * this.#cw; 919 + } 920 + 909 921 get width() { 910 - const visibleText = stripColorCodes(this.txt); 911 - return visibleText.length * this.#cw + this.#gap * 2; 922 + return this.#measureTextWidth(this.txt) + this.#gap * 2; 912 923 } 913 924 914 925 get height() { ··· 922 933 #computePosition(text, pos = { x: 0, y: 0 }) { 923 934 pos = { ...pos }; // Make a shallow copy of pos because we will mutate it. 924 935 let x, y; 925 - const visibleText = stripColorCodes(text); 926 - const w = visibleText.length * this.#cw + this.#g2; 936 + const w = this.#measureTextWidth(text) + this.#g2; 927 937 const h = this.#h; 928 938 929 939 if (pos.screen) {
+2 -2
vscode-extension/package-lock.json
··· 1 1 { 2 2 "name": "aesthetic-computer-code", 3 - "version": "1.270.0", 3 + "version": "1.271.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "aesthetic-computer-code", 9 - "version": "1.270.0", 9 + "version": "1.271.0", 10 10 "license": "None", 11 11 "dependencies": { 12 12 "acorn": "^8.15.0",
+1 -1
vscode-extension/package.json
··· 4 4 "displayName": "Aesthetic Computer", 5 5 "icon": "resources/icon.png", 6 6 "author": "Jeffrey Alan Scudder", 7 - "version": "1.270.0", 7 + "version": "1.271.0", 8 8 "description": "Code, run, and publish your pieces. Includes Aesthetic Computer themes and KidLisp syntax highlighting.", 9 9 "engines": { 10 10 "vscode": "^1.105.0"