@kecan0406/ttheme 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ttheme.js CHANGED
@@ -4098,6 +4098,9 @@ var require_src = __commonJS((exports, module) => {
4098
4098
  module.exports = { cursor, scroll, erase, beep };
4099
4099
  });
4100
4100
 
4101
+ // src/bin.ts
4102
+ import { isMainThread as isMainThread2 } from "node:worker_threads";
4103
+
4101
4104
  // node_modules/commander/lib/error.js
4102
4105
  class CommanderError extends Error {
4103
4106
  constructor(exitCode, code, message) {
@@ -6144,7 +6147,7 @@ var program = new Command;
6144
6147
  // package.json
6145
6148
  var package_default = {
6146
6149
  name: "@kecan0406/ttheme",
6147
- version: "0.1.3",
6150
+ version: "0.1.4",
6148
6151
  type: "module",
6149
6152
  bin: {
6150
6153
  ttheme: "bin/ttheme.js"
@@ -7042,12 +7045,12 @@ ${themes.length} themes (${rotation(themes).length} in the new-tab rotation) ->
7042
7045
 
7043
7046
  // src/find.ts
7044
7047
  import { spawn as spawn2 } from "node:child_process";
7045
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
7048
+ import { existsSync as existsSync3, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
7046
7049
  import { tmpdir } from "node:os";
7047
- import { dirname as dirname3, join as join7 } from "node:path";
7050
+ import { dirname as dirname4, join as join7 } from "node:path";
7048
7051
 
7049
7052
  // src/backdrop.ts
7050
- import { mkdirSync, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync } from "node:fs";
7053
+ import { mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync, rmSync as rmSync2, statSync, utimesSync, writeFileSync } from "node:fs";
7051
7054
  import { join as join3 } from "node:path";
7052
7055
 
7053
7056
  // src/png.ts
@@ -7231,7 +7234,11 @@ var FILL = { width: 2560, height: 1550 };
7231
7234
  var FIGURE = 2560;
7232
7235
  var FAINT = 0.1;
7233
7236
  var HEADROOM = 0.15;
7237
+ var KEEP = 0.55;
7238
+ var PLACE = 0.65;
7239
+ var STANDS = 3;
7234
7240
  var PEAK = luminance(mix("#19161e", "#9b86c8", 0.2));
7241
+ var SHELF = "shelf";
7235
7242
  function mix(a, b, t) {
7236
7243
  const from = rgb(a);
7237
7244
  const to = rgb(b);
@@ -7321,11 +7328,14 @@ function figureBox(image) {
7321
7328
  function headAnchor(box) {
7322
7329
  return Math.min(0.5, (HEADROOM + 0.5) * (box.w * FILL.height / FILL.width / box.h));
7323
7330
  }
7331
+ function band(box, h, anchor) {
7332
+ const top = Math.min(Math.max(0, anchor * box.h - h / 2), box.h - h);
7333
+ return { x: box.x, y: box.y + top, w: box.w, h };
7334
+ }
7324
7335
  function fillBox(box, width, height, anchor) {
7325
7336
  const h = box.w * height / width;
7326
7337
  if (h <= box.h) {
7327
- const top = Math.min(Math.max(0, anchor * box.h - h / 2), box.h - h);
7328
- return { x: box.x, y: box.y + top, w: box.w, h };
7338
+ return band(box, h, anchor);
7329
7339
  }
7330
7340
  const w = box.h * width / height;
7331
7341
  return { x: box.x + (box.w - w) / 2, y: box.y, w, h: box.h };
@@ -7342,20 +7352,66 @@ function figure(image, box) {
7342
7352
  const k = Math.min(1, FIGURE / Math.max(box.w, box.h));
7343
7353
  return resample(image, box, Math.round(box.w * k), Math.round(box.h * k));
7344
7354
  }
7345
- function fill(image, box, anchor) {
7346
- return resample(image, fillBox(box, FILL.width, FILL.height, anchor), FILL.width, FILL.height);
7355
+ function fillFrame(box, width, height, anchor, clear) {
7356
+ const keep = KEEP * box.h;
7357
+ const span = box.w * height / width;
7358
+ if (clear < STANDS || span > keep) {
7359
+ return { crop: fillBox(box, width, height, anchor), at: { x: 0, y: 0, w: width, h: height }, inset: false };
7360
+ }
7361
+ const short = 1 - span / keep;
7362
+ const h = height * (1 - HEADROOM * short);
7363
+ const w = box.w * h / keep;
7364
+ return {
7365
+ crop: band(box, keep, anchor),
7366
+ at: { x: (width - w) * PLACE, y: height - h, w, h },
7367
+ inset: true
7368
+ };
7369
+ }
7370
+ function paint(image, frame, width, height) {
7371
+ const w = Math.max(1, Math.round(frame.at.w));
7372
+ const h = Math.max(1, Math.round(frame.at.h));
7373
+ const cut = resample(image, frame.crop, w, h);
7374
+ const out = new Uint8Array(width * height * 4);
7375
+ const ox = Math.round(frame.at.x);
7376
+ const oy = Math.round(frame.at.y);
7377
+ for (let y = 0;y < h; y++) {
7378
+ const ty = oy + y;
7379
+ const from = Math.max(0, -ox);
7380
+ const to = Math.min(w, width - ox);
7381
+ if (ty < 0 || ty >= height || to <= from) {
7382
+ continue;
7383
+ }
7384
+ out.set(cut.data.subarray((y * w + from) * 4, (y * w + to) * 4), (ty * width + ox + from) * 4);
7385
+ }
7386
+ return { width, height, data: out };
7387
+ }
7388
+ function fill(image, box, anchor, clear) {
7389
+ const frame = fillFrame(box, FILL.width, FILL.height, anchor, clear);
7390
+ return frame.inset ? paint(image, frame, FILL.width, FILL.height) : resample(image, frame.crop, FILL.width, FILL.height);
7347
7391
  }
7348
- function tryOn(image, colors, tone, width, height) {
7392
+ function tryOn(image, colors, tone, width, height, clear) {
7349
7393
  const box = figureBox(image);
7350
- const view = coverBox(fillBox(box, FILL.width, FILL.height, headAnchor(box)), width, height);
7351
- return composite(tint(resample(image, view, width, height), colors.background, tone.color), colors.background, tone.opacity);
7394
+ const frame = fillFrame(box, FILL.width, FILL.height, headAnchor(box), clear);
7395
+ let shown;
7396
+ if (frame.inset) {
7397
+ const view = coverBox({ x: 0, y: 0, w: FILL.width, h: FILL.height }, width, height);
7398
+ const k = width / view.w;
7399
+ shown = paint(image, {
7400
+ ...frame,
7401
+ at: { x: (frame.at.x - view.x) * k, y: (frame.at.y - view.y) * k, w: frame.at.w * k, h: frame.at.h * k }
7402
+ }, width, height);
7403
+ } else {
7404
+ shown = resample(image, coverBox(frame.crop, width, height), width, height);
7405
+ }
7406
+ return composite(tint(shown, colors.background, tone.color), colors.background, tone.opacity);
7352
7407
  }
7353
7408
  function backgroundsDir(configHome) {
7354
7409
  return join3(configHome, "ttheme", "backgrounds");
7355
7410
  }
7356
- function backdropConf(name, fillPath, opacity, from) {
7411
+ function backdropConf(name, fillPath, opacity, from, key) {
7357
7412
  return [
7358
7413
  ...from ? [`# from ${from}`] : [],
7414
+ ...key ? [`# image ${key}`] : [],
7359
7415
  `background-image = ${fillPath}`,
7360
7416
  "background-image-fit = cover",
7361
7417
  "background-image-position = center",
@@ -7373,26 +7429,139 @@ function clearBackdrop(dir, name) {
7373
7429
  }
7374
7430
  }
7375
7431
  }
7432
+ function imageKey(origin) {
7433
+ return `${origin.site}_${origin.id}`;
7434
+ }
7435
+ function shelfDir(dir, name) {
7436
+ return join3(dir, SHELF, name);
7437
+ }
7438
+ function belongs(file, name) {
7439
+ const rest = file.slice(name.length);
7440
+ return file.startsWith(name) && (rest === ".png" || rest === ".conf" || /^@fill-\d+\.png$/.test(rest));
7441
+ }
7442
+ function activeKey(dir, name) {
7443
+ try {
7444
+ return /^# image (\S+)$/m.exec(readFileSync2(join3(dir, `${name}.conf`), "utf8"))?.[1];
7445
+ } catch {
7446
+ return;
7447
+ }
7448
+ }
7449
+ function shelved(dir, name) {
7450
+ try {
7451
+ return readdirSync2(shelfDir(dir, name));
7452
+ } catch {
7453
+ return [];
7454
+ }
7455
+ }
7456
+ function imageKeys(dir, name) {
7457
+ const active = activeKey(dir, name);
7458
+ return [...active ? [active] : [], ...shelved(dir, name)].sort();
7459
+ }
7460
+ function shelve(dir, name) {
7461
+ const key = activeKey(dir, name);
7462
+ if (!key) {
7463
+ return;
7464
+ }
7465
+ const to = join3(shelfDir(dir, name), key);
7466
+ rmSync2(to, { recursive: true, force: true });
7467
+ mkdirSync(to, { recursive: true });
7468
+ for (const file of readdirSync2(dir)) {
7469
+ if (belongs(file, name)) {
7470
+ renameSync(join3(dir, file), join3(to, file.slice(name.length)));
7471
+ }
7472
+ }
7473
+ clearBackdrop(dir, name);
7474
+ }
7475
+ function unshelve(dir, name, key) {
7476
+ const from = join3(shelfDir(dir, name), key);
7477
+ for (const file of readdirSync2(from)) {
7478
+ renameSync(join3(from, file), join3(dir, `${name}${file}`));
7479
+ }
7480
+ rmSync2(from, { recursive: true });
7481
+ }
7482
+ function originalOf(dir, name, key) {
7483
+ try {
7484
+ return readdirSync2(join3(dir, "originals")).find((file) => file.startsWith(`${name}-${key}.`));
7485
+ } catch {
7486
+ return;
7487
+ }
7488
+ }
7489
+ function newest(dir, name, key) {
7490
+ const file = originalOf(dir, name, key);
7491
+ if (file) {
7492
+ const now = new Date;
7493
+ utimesSync(join3(dir, "originals", file), now, now);
7494
+ }
7495
+ }
7496
+ function switchImage(configHome, name, step) {
7497
+ const dir = backgroundsDir(configHome);
7498
+ const keys = imageKeys(dir, name);
7499
+ const now = activeKey(dir, name);
7500
+ if (!now || keys.length < 2) {
7501
+ throw new Error(`${name} has no other image`);
7502
+ }
7503
+ const at2 = (keys.indexOf(now) + step + keys.length) % keys.length;
7504
+ const key = keys[at2];
7505
+ shelve(dir, name);
7506
+ unshelve(dir, name, key);
7507
+ newest(dir, name, key);
7508
+ return { key, at: at2 + 1, of: keys.length };
7509
+ }
7510
+ function dropImage(configHome, name) {
7511
+ const dir = backgroundsDir(configHome);
7512
+ const files = readdirSync2(dir).filter((file) => belongs(file, name));
7513
+ if (files.length === 0) {
7514
+ throw new Error(`${name} has no image`);
7515
+ }
7516
+ const keys = imageKeys(dir, name);
7517
+ const now = activeKey(dir, name);
7518
+ const original = now && originalOf(dir, name, now);
7519
+ if (original) {
7520
+ rmSync2(join3(dir, "originals", original));
7521
+ }
7522
+ for (const file of files) {
7523
+ rmSync2(join3(dir, file));
7524
+ }
7525
+ clearBackdrop(dir, name);
7526
+ const rest = keys.filter((key) => key !== now);
7527
+ const next = rest[(now ? keys.indexOf(now) : 0) % rest.length];
7528
+ if (next) {
7529
+ unshelve(dir, name, next);
7530
+ newest(dir, name, next);
7531
+ }
7532
+ return { key: now, left: rest.length };
7533
+ }
7534
+ function retire(dir, name, key) {
7535
+ if (activeKey(dir, name) !== key) {
7536
+ shelve(dir, name);
7537
+ }
7538
+ if (key) {
7539
+ rmSync2(join3(shelfDir(dir, name), key), { recursive: true, force: true });
7540
+ }
7541
+ clearBackdrop(dir, name);
7542
+ }
7376
7543
  function installBackdrop(configHome, colors, tone, image, original) {
7377
7544
  const dir = backgroundsDir(configHome);
7378
7545
  const name = colors.name;
7546
+ const key = imageKey(original);
7379
7547
  mkdirSync(join3(dir, "originals"), { recursive: true });
7380
- clearBackdrop(dir, name);
7548
+ retire(dir, name, key);
7381
7549
  const box = figureBox(image);
7382
7550
  const anchor = headAnchor(box);
7551
+ const clear = transparency(image);
7383
7552
  const whole = join3(dir, `${name}.png`);
7384
7553
  const fillPath = join3(dir, `${name}@fill-${Math.round(anchor * 100)}.png`);
7385
7554
  const conf = join3(dir, `${name}.conf`);
7386
7555
  const source = join3(dir, "originals", `${name}-${original.site}_${original.id}.${original.ext}`);
7387
7556
  writeFileSync(whole, encodePng(tint(figure(image, box), colors.background, tone.color)));
7388
- writeFileSync(fillPath, encodePng(tint(fill(image, box, anchor), colors.background, tone.color)));
7389
- writeFileSync(conf, backdropConf(name, fillPath, tone.opacity, original.from));
7557
+ writeFileSync(fillPath, encodePng(tint(fill(image, box, anchor, clear), colors.background, tone.color)));
7558
+ writeFileSync(conf, backdropConf(name, fillPath, tone.opacity, original.from, key));
7390
7559
  writeFileSync(source, original.bytes);
7391
7560
  return [whole, fillPath, conf, source];
7392
7561
  }
7393
7562
  function origins(configHome) {
7394
7563
  const dir = join3(backgroundsDir(configHome), "originals");
7395
- const newest = new Map;
7564
+ const newest2 = new Map;
7396
7565
  let files;
7397
7566
  try {
7398
7567
  files = readdirSync2(dir);
@@ -7406,21 +7575,23 @@ function origins(configHome) {
7406
7575
  continue;
7407
7576
  }
7408
7577
  const at2 = statSync(join3(dir, file)).mtimeMs;
7409
- const known = newest.get(name);
7578
+ const known = newest2.get(name);
7410
7579
  if (!known || at2 > known.at) {
7411
- newest.set(name, { site, id: Number(id), at: at2 });
7580
+ newest2.set(name, { site, id: Number(id), at: at2 });
7412
7581
  }
7413
7582
  }
7414
- return new Map([...newest].map(([name, { site, id }]) => [name, { site, id }]));
7583
+ return new Map([...newest2].map(([name, { site, id }]) => [name, { site, id }]));
7415
7584
  }
7416
7585
 
7417
7586
  // src/booru.ts
7587
+ import { readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
7418
7588
  import { setDefaultAutoSelectFamilyAttemptTimeout } from "node:net";
7419
7589
  import { homedir } from "node:os";
7420
7590
  import { join as join4 } from "node:path";
7421
7591
  import { setTimeout as sleep } from "node:timers/promises";
7422
7592
  var PAGE = 100;
7423
7593
  var MAX_PIXELS = 25000000;
7594
+ var CACHE_BYTES = 512 * 1024 * 1024;
7424
7595
  var RATINGS = ["safe", "questionable", "explicit"];
7425
7596
  var BLOCKS = ["nudity", "underwear"];
7426
7597
  var EXPOSED = {
@@ -7435,10 +7606,18 @@ var CONNECT = 3000;
7435
7606
  var MAX_WAIT = 60000;
7436
7607
  var TRIES = 3;
7437
7608
  var AGENT = `ttheme/${package_default.version} (+${package_default.homepage})`;
7609
+ var HELD = ["orig", "tile", "thumb", "cut"];
7438
7610
  var paused = new Map;
7439
7611
  var HOSTS = hostMap(process.env.TTHEME_FIND_HOSTS);
7440
7612
  var CUTOUTS = cutoutMap(process.env.TTHEME_FIND_CUTOUTS);
7441
7613
  setDefaultAutoSelectFamilyAttemptTimeout(CONNECT);
7614
+ function listing(dir) {
7615
+ try {
7616
+ return readdirSync3(dir);
7617
+ } catch {
7618
+ return [];
7619
+ }
7620
+ }
7442
7621
  function absolute(url) {
7443
7622
  return url.startsWith("//") ? `https:${url}` : url;
7444
7623
  }
@@ -7762,8 +7941,38 @@ function mates(owners) {
7762
7941
  }
7763
7942
  return byOwner;
7764
7943
  }
7944
+ function cacheRoot() {
7945
+ return join4(process.env.XDG_CACHE_HOME ?? join4(homedir(), ".cache"), "ttheme");
7946
+ }
7765
7947
  function cacheDir(site) {
7766
- return join4(process.env.XDG_CACHE_HOME ?? join4(homedir(), ".cache"), "ttheme", site.key);
7948
+ return join4(cacheRoot(), site.key);
7949
+ }
7950
+ function sweepCache(limit = CACHE_BYTES, root2 = cacheRoot()) {
7951
+ const files = [];
7952
+ let total = 0;
7953
+ for (const site of listing(root2)) {
7954
+ for (const kind of HELD) {
7955
+ const dir = join4(root2, site, kind);
7956
+ for (const name of listing(dir)) {
7957
+ const path2 = join4(dir, name);
7958
+ try {
7959
+ const { size, mtimeMs } = statSync2(path2);
7960
+ files.push({ path: path2, size, at: mtimeMs });
7961
+ total += size;
7962
+ } catch {}
7963
+ }
7964
+ }
7965
+ }
7966
+ files.sort((a, b) => a.at - b.at);
7967
+ for (const file of files) {
7968
+ if (total <= limit) {
7969
+ return;
7970
+ }
7971
+ try {
7972
+ rmSync3(file.path, { force: true });
7973
+ total -= file.size;
7974
+ } catch {}
7975
+ }
7767
7976
  }
7768
7977
  function retryAfter(value, now) {
7769
7978
  if (value !== null && /^\s*\d+\s*$/.test(value)) {
@@ -7846,7 +8055,7 @@ async function fetchBytes(site, url, signal, progress) {
7846
8055
  }
7847
8056
 
7848
8057
  // src/catalog.ts
7849
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
8058
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
7850
8059
  import { dirname, join as join5 } from "node:path";
7851
8060
  var REGISTRY_URL = "https://kecan0406.github.io/ttheme/manifest.json";
7852
8061
  function catalogPath(configHome) {
@@ -7863,7 +8072,7 @@ function readCatalog(configHome) {
7863
8072
  if (!existsSync(path2)) {
7864
8073
  throw new Error(`no catalog at ${path2} — run \`ttheme init\` first`);
7865
8074
  }
7866
- return parseCatalog(readFileSync2(path2, "utf8"));
8075
+ return parseCatalog(readFileSync3(path2, "utf8"));
7867
8076
  }
7868
8077
  function parseCatalog(source) {
7869
8078
  let doc;
@@ -7925,7 +8134,6 @@ function search(palettes, query) {
7925
8134
 
7926
8135
  // src/cutout.ts
7927
8136
  import { spawn } from "node:child_process";
7928
- import { readFileSync as readFileSync3 } from "node:fs";
7929
8137
  var TIMEOUT2 = 30000;
7930
8138
  var LEAST = 3;
7931
8139
  var MOST = 97;
@@ -7967,16 +8175,12 @@ function removeBackground(input, output, signal) {
7967
8175
  });
7968
8176
  child.on("error", reject);
7969
8177
  child.on("close", (code) => {
7970
- if (code !== 0) {
7971
- reject(new Error(failure.trim().split(`
7972
- `).at(-1) || `osascript exited ${code}`));
8178
+ if (code === 0) {
8179
+ resolve();
7973
8180
  return;
7974
8181
  }
7975
- try {
7976
- resolve(decodePng(new Uint8Array(readFileSync3(output))));
7977
- } catch (error) {
7978
- reject(error);
7979
- }
8182
+ reject(new Error(failure.trim().split(`
8183
+ `).at(-1) || `osascript exited ${code}`));
7980
8184
  });
7981
8185
  });
7982
8186
  }
@@ -8530,7 +8734,7 @@ function renderFind(view, cols, rows) {
8530
8734
  }
8531
8735
 
8532
8736
  // src/palettes.ts
8533
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
8737
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "node:fs";
8534
8738
  import { homedir as homedir2 } from "node:os";
8535
8739
  import { dirname as dirname2, join as join6 } from "node:path";
8536
8740
  var EMITTERS = { ghostty, kitty, alacritty };
@@ -8658,7 +8862,7 @@ function forget(configHome2, catalog, terminals, names) {
8658
8862
  for (const { file } of themeFiles(terminal, toTheme(entry, catalog))) {
8659
8863
  const path2 = join6(configHome2, terminal, "themes", file);
8660
8864
  if (existsSync2(path2)) {
8661
- rmSync3(path2, { force: true });
8865
+ rmSync4(path2, { force: true });
8662
8866
  removed.push(path2);
8663
8867
  }
8664
8868
  }
@@ -8667,6 +8871,117 @@ function forget(configHome2, catalog, terminals, names) {
8667
8871
  return removed;
8668
8872
  }
8669
8873
 
8874
+ // src/render.ts
8875
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
8876
+ import { dirname as dirname3 } from "node:path";
8877
+ import { isMainThread, parentPort, Worker } from "node:worker_threads";
8878
+ var LANES = { thumb: "tile", show: "view", backdrop: "view" };
8879
+ function work(task) {
8880
+ const image = decodeImage(new Uint8Array(readFileSync5(task.from)), MAX_PIXELS);
8881
+ if (task.job === "backdrop") {
8882
+ installBackdrop(task.home, task.colors, task.tone, image, {
8883
+ ...task.origin,
8884
+ bytes: new Uint8Array(readFileSync5(task.source))
8885
+ });
8886
+ return 0;
8887
+ }
8888
+ if (task.job === "thumb") {
8889
+ mkdirSync4(dirname3(task.to), { recursive: true });
8890
+ writeFileSync4(task.to, encodePng(contain(image, task.width, task.height)));
8891
+ return 0;
8892
+ }
8893
+ const clear = transparency(image);
8894
+ mkdirSync4(dirname3(task.to), { recursive: true });
8895
+ writeFileSync4(task.to, encodePng(tryOn(image, task.colors, task.tone, task.width, task.height, clear)));
8896
+ return clear;
8897
+ }
8898
+ function serveRenders(port) {
8899
+ port.on("message", (task) => {
8900
+ try {
8901
+ port.postMessage({ id: task.id, value: work(task) });
8902
+ } catch (error) {
8903
+ port.postMessage({ id: task.id, error: error instanceof Error ? error.message : String(error) });
8904
+ }
8905
+ });
8906
+ }
8907
+
8908
+ class Line2 {
8909
+ worker;
8910
+ next = 1;
8911
+ waiting = new Map;
8912
+ send(task) {
8913
+ const id = this.next++;
8914
+ return new Promise((ok, no) => {
8915
+ const worker = this.open();
8916
+ this.waiting.set(id, { ok, no });
8917
+ worker.ref();
8918
+ worker.postMessage({ ...task, id });
8919
+ });
8920
+ }
8921
+ close() {
8922
+ this.worker?.terminate();
8923
+ this.worker = undefined;
8924
+ this.drop(new Error("renderer closed"));
8925
+ }
8926
+ open() {
8927
+ if (this.worker) {
8928
+ return this.worker;
8929
+ }
8930
+ const worker = new Worker(import.meta.filename);
8931
+ worker.unref();
8932
+ worker.on("message", ({ id, value, error }) => {
8933
+ const held = this.waiting.get(id);
8934
+ this.waiting.delete(id);
8935
+ if (this.waiting.size === 0) {
8936
+ worker.unref();
8937
+ }
8938
+ if (error !== undefined) {
8939
+ held?.no(new Error(error));
8940
+ } else {
8941
+ held?.ok(value);
8942
+ }
8943
+ });
8944
+ const lost = (error) => {
8945
+ if (this.worker === worker) {
8946
+ this.worker = undefined;
8947
+ }
8948
+ this.drop(error instanceof Error ? error : new Error("renderer stopped"));
8949
+ };
8950
+ worker.on("error", lost);
8951
+ worker.on("exit", () => lost(undefined));
8952
+ this.worker = worker;
8953
+ return worker;
8954
+ }
8955
+ drop(error) {
8956
+ for (const held of this.waiting.values()) {
8957
+ held.no(error);
8958
+ }
8959
+ this.waiting.clear();
8960
+ }
8961
+ }
8962
+
8963
+ class Renderer {
8964
+ lines = new Map;
8965
+ run(task) {
8966
+ const lane = LANES[task.job];
8967
+ let line = this.lines.get(lane);
8968
+ if (!line) {
8969
+ line = new Line2;
8970
+ this.lines.set(lane, line);
8971
+ }
8972
+ return line.send(task);
8973
+ }
8974
+ close() {
8975
+ for (const line of this.lines.values()) {
8976
+ line.close();
8977
+ }
8978
+ this.lines.clear();
8979
+ }
8980
+ }
8981
+ if (!isMainThread && parentPort) {
8982
+ serveRenders(parentPort);
8983
+ }
8984
+
8670
8985
  // src/unblock.ts
8671
8986
  import { connect, createServer } from "node:net";
8672
8987
  var SPLIT = 20;
@@ -8801,12 +9116,13 @@ function initial(setting, raw) {
8801
9116
  }
8802
9117
  return setting.choices.find((choice) => choice === raw) ?? setting.choices[0];
8803
9118
  }
8804
- var WORKERS = 4;
9119
+ var PROBE = 12;
9120
+ var THUMB = 12;
8805
9121
  var PRELOAD = 2;
8806
9122
  var SETTLE = 150;
8807
9123
  var ESCAPE = 30;
8808
9124
  var TRY_WIDTH = 1280;
8809
- var LOOKAHEAD = 1;
9125
+ var LOOKAHEAD = 2;
8810
9126
  function blank(key, searching) {
8811
9127
  return {
8812
9128
  key,
@@ -8845,19 +9161,16 @@ function incomplete(input) {
8845
9161
  const at2 = input.lastIndexOf("\x1B");
8846
9162
  return at2 !== -1 && /^\[?[0-9;]*$/.test(input.slice(at2 + 1));
8847
9163
  }
8848
- function tick() {
8849
- return new Promise((resolve2) => setImmediate(resolve2));
8850
- }
8851
9164
  function readCache(site, name) {
8852
9165
  try {
8853
- return JSON.parse(readFileSync5(join7(cacheDir(site), name), "utf8"));
9166
+ return JSON.parse(readFileSync6(join7(cacheDir(site), name), "utf8"));
8854
9167
  } catch {
8855
9168
  return {};
8856
9169
  }
8857
9170
  }
8858
9171
  function writeCache(site, name, data) {
8859
- mkdirSync4(cacheDir(site), { recursive: true });
8860
- writeFileSync4(join7(cacheDir(site), name), `${JSON.stringify(data)}
9172
+ mkdirSync5(cacheDir(site), { recursive: true });
9173
+ writeFileSync5(join7(cacheDir(site), name), `${JSON.stringify(data)}
8861
9174
  `);
8862
9175
  }
8863
9176
 
@@ -8884,6 +9197,8 @@ class Finder {
8884
9197
  unsaved = new Set;
8885
9198
  thumbQueue = [];
8886
9199
  thumbing = 0;
9200
+ clarity = new Map;
9201
+ renders = new Renderer;
8887
9202
  sent = new Map;
8888
9203
  placed = new Map;
8889
9204
  current;
@@ -8955,7 +9270,7 @@ class Finder {
8955
9270
  return `${site.key}:${id}`;
8956
9271
  }
8957
9272
  origPath(site, id, ext) {
8958
- return join7(this.scratch, "orig", `${site.key}-${id}.${ext}`);
9273
+ return join7(cacheDir(site), "orig", `${id}.${ext}`);
8959
9274
  }
8960
9275
  tileOf(site, post2, variants) {
8961
9276
  const version2 = rendition(post2) ?? post2;
@@ -9024,7 +9339,10 @@ class Finder {
9024
9339
  this.write("\x1B_Ga=d,d=A,q=2\x1B\\\x1B[H\x1B[J");
9025
9340
  stdin.setRawMode(false);
9026
9341
  stdin.pause();
9027
- rmSync4(this.scratch, { recursive: true, force: true });
9342
+ this.saveProbes();
9343
+ this.renders.close();
9344
+ rmSync5(this.scratch, { recursive: true, force: true });
9345
+ sweepCache();
9028
9346
  return code;
9029
9347
  }
9030
9348
  write(text) {
@@ -9085,7 +9403,7 @@ class Finder {
9085
9403
  this.rows = process.stdout.rows || this.rows;
9086
9404
  this.scroll();
9087
9405
  if (this.view.mode === "try" && this.current) {
9088
- this.present();
9406
+ this.reveal();
9089
9407
  }
9090
9408
  this.pump();
9091
9409
  this.draw();
@@ -9260,9 +9578,9 @@ class Finder {
9260
9578
  save(changed) {
9261
9579
  const path2 = join7(this.home, "ttheme", "config.zsh");
9262
9580
  try {
9263
- const before = existsSync3(path2) ? readFileSync5(path2, "utf8") : "";
9581
+ const before = existsSync3(path2) ? readFileSync6(path2, "utf8") : "";
9264
9582
  const after = changed.reduce((text, setting) => withSetting(text, setting.name, this.setting(setting.name)), before);
9265
- writeFileSync4(path2, after);
9583
+ writeFileSync5(path2, after);
9266
9584
  } catch (error) {
9267
9585
  this.view.error = error instanceof Error ? error.message : String(error);
9268
9586
  }
@@ -9489,12 +9807,12 @@ class Finder {
9489
9807
  this.show();
9490
9808
  this.draw();
9491
9809
  const tags = this.query(site);
9810
+ this.tally(gen, site, board, tags);
9492
9811
  try {
9493
- const [total, owners] = await Promise.all([fetchCount(site, tags, this.signal), this.mateOwners(site)]);
9812
+ const owners = await this.mateOwners(site);
9494
9813
  if (gen !== this.gen) {
9495
9814
  return;
9496
9815
  }
9497
- board.total = total;
9498
9816
  const room = tagsOf(tags) + 1 <= site.tagBudget;
9499
9817
  board.sources = [
9500
9818
  ...room ? [...owners.keys()].map((owner) => ({ tags: `${tags} user:${owner}`, page: 0, done: false })) : [],
@@ -9507,6 +9825,16 @@ class Finder {
9507
9825
  this.fail(gen, error);
9508
9826
  }
9509
9827
  }
9828
+ async tally(gen, site, board, tags) {
9829
+ try {
9830
+ const total = await fetchCount(site, tags, this.signal);
9831
+ if (gen === this.gen) {
9832
+ board.total = total;
9833
+ this.show();
9834
+ this.draw();
9835
+ }
9836
+ } catch {}
9837
+ }
9510
9838
  fail(gen, error) {
9511
9839
  if (gen !== this.gen || this.signal.aborted) {
9512
9840
  return;
@@ -9521,6 +9849,24 @@ class Finder {
9521
9849
  const { perRow, rowsVis } = gridShape(this.cols, this.rows);
9522
9850
  return this.view.tiles.length < (this.board.top + rowsVis + LOOKAHEAD) * perRow;
9523
9851
  }
9852
+ async nextPage(site, board, gen) {
9853
+ const source = board.sources.find((s) => !s.done);
9854
+ if (!source) {
9855
+ return;
9856
+ }
9857
+ const posts = await fetchPosts(site, source.tags, source.page, this.signal);
9858
+ if (gen !== this.gen) {
9859
+ return;
9860
+ }
9861
+ source.page++;
9862
+ source.done = posts.length < PAGE;
9863
+ for (const post2 of posts) {
9864
+ if (!board.seen.has(post2.id)) {
9865
+ board.seen.add(post2.id);
9866
+ board.queue.push(post2);
9867
+ }
9868
+ }
9869
+ }
9524
9870
  async pump() {
9525
9871
  const gen = this.gen;
9526
9872
  const board = this.board;
@@ -9529,40 +9875,40 @@ class Finder {
9529
9875
  }
9530
9876
  this.pumping = gen;
9531
9877
  const site = this.site;
9878
+ const flight = [];
9879
+ let page;
9532
9880
  try {
9533
9881
  board.searching = true;
9534
- while (gen === this.gen && this.wants()) {
9535
- if (board.queue.length === 0) {
9536
- const source = board.sources.find((s) => !s.done);
9537
- if (!source) {
9538
- break;
9539
- }
9540
- const posts = await fetchPosts(site, source.tags, source.page, this.signal);
9541
- if (gen !== this.gen) {
9542
- return;
9543
- }
9544
- source.page++;
9545
- source.done = posts.length < PAGE;
9546
- for (const post2 of posts) {
9547
- if (!board.seen.has(post2.id)) {
9548
- board.seen.add(post2.id);
9549
- board.queue.push(post2);
9882
+ while (gen === this.gen) {
9883
+ while (flight.length < PROBE && board.queue.length > 0 && this.wants()) {
9884
+ const post2 = board.queue.shift();
9885
+ flight.push({ post: post2, passed: this.passes(site, post2) });
9886
+ }
9887
+ if (!page && board.queue.length < PROBE && board.sources.some((s) => !s.done) && this.wants()) {
9888
+ const job = this.nextPage(site, board, gen).then(() => {
9889
+ if (page === job) {
9890
+ page = undefined;
9550
9891
  }
9892
+ });
9893
+ job.catch(() => {});
9894
+ page = job;
9895
+ }
9896
+ const head = flight.shift();
9897
+ if (!head) {
9898
+ if (!page) {
9899
+ break;
9551
9900
  }
9901
+ await page;
9552
9902
  continue;
9553
9903
  }
9554
- const batch = board.queue.splice(0, WORKERS);
9555
- const passed = await Promise.all(batch.map((post2) => this.passes(site, post2)));
9556
- this.saveProbes(site);
9904
+ const passed = await head.passed;
9557
9905
  if (gen !== this.gen) {
9558
9906
  return;
9559
9907
  }
9560
- batch.forEach((post2, i) => {
9561
- board.checked++;
9562
- if (passed[i]) {
9563
- this.admit(site, post2);
9564
- }
9565
- });
9908
+ board.checked++;
9909
+ if (passed) {
9910
+ this.admit(site, head.post);
9911
+ }
9566
9912
  this.show();
9567
9913
  this.draw();
9568
9914
  }
@@ -9574,6 +9920,7 @@ class Finder {
9574
9920
  } catch (error) {
9575
9921
  this.fail(gen, error);
9576
9922
  } finally {
9923
+ this.saveProbes();
9577
9924
  if (this.pumping === gen) {
9578
9925
  this.pumping = 0;
9579
9926
  }
@@ -9615,10 +9962,14 @@ class Finder {
9615
9962
  return false;
9616
9963
  }
9617
9964
  }
9618
- saveProbes(site) {
9619
- if (this.unsaved.delete(site.key)) {
9620
- writeCache(site, "probes.json", this.probes(site));
9965
+ saveProbes() {
9966
+ for (const key of this.unsaved) {
9967
+ const site = SITES.find((s) => s.key === key);
9968
+ if (site) {
9969
+ writeCache(site, "probes.json", this.probes(site));
9970
+ }
9621
9971
  }
9972
+ this.unsaved.clear();
9622
9973
  }
9623
9974
  admit(site, post2) {
9624
9975
  this.posts.set(this.mark(site, post2.id), post2);
@@ -9644,22 +9995,27 @@ class Finder {
9644
9995
  }
9645
9996
  const found = origins(this.home);
9646
9997
  const known = readCache(site, "owners.json");
9647
- const byPalette = new Map;
9648
- for (const sibling of this.catalog.palettes) {
9998
+ const siblings = this.catalog.palettes.flatMap((sibling) => {
9649
9999
  const origin = found.get(sibling.name);
9650
- if (sibling.group !== this.entry.group || sibling.name === this.entry.name || origin?.site !== site.key) {
9651
- continue;
10000
+ return sibling.group === this.entry.group && sibling.name !== this.entry.name && origin?.site === site.key ? [{ name: sibling.name, id: origin.id }] : [];
10001
+ });
10002
+ const asked = await Promise.all(siblings.map(async (sibling) => {
10003
+ const owner = known[sibling.id];
10004
+ if (owner !== undefined) {
10005
+ return { ...sibling, owner };
9652
10006
  }
9653
- let owner = known[origin.id];
9654
- if (owner === undefined) {
9655
- try {
9656
- owner = (await fetchPost(site, origin.id, this.signal))?.owner ?? "";
9657
- known[origin.id] = owner;
9658
- } catch {
9659
- continue;
9660
- }
10007
+ try {
10008
+ return { ...sibling, owner: (await fetchPost(site, sibling.id, this.signal))?.owner ?? "" };
10009
+ } catch {
10010
+ return;
10011
+ }
10012
+ }));
10013
+ const byPalette = new Map;
10014
+ for (const sibling of asked) {
10015
+ if (sibling) {
10016
+ known[sibling.id] = sibling.owner;
10017
+ byPalette.set(sibling.name, sibling.owner);
9661
10018
  }
9662
- byPalette.set(sibling.name, owner);
9663
10019
  }
9664
10020
  writeCache(site, "owners.json", known);
9665
10021
  const owners = mates(byPalette);
@@ -9668,7 +10024,7 @@ class Finder {
9668
10024
  }
9669
10025
  async cached(site, path2, url, progress2) {
9670
10026
  if (existsSync3(path2)) {
9671
- return new Uint8Array(readFileSync5(path2));
10027
+ return statSync3(path2).size;
9672
10028
  }
9673
10029
  const running = this.inflight.get(path2);
9674
10030
  if (running) {
@@ -9676,9 +10032,9 @@ class Finder {
9676
10032
  }
9677
10033
  const job = (async () => {
9678
10034
  const bytes = await fetchBytes(site, url, this.signal, progress2);
9679
- mkdirSync4(dirname3(path2), { recursive: true });
9680
- writeFileSync4(path2, bytes);
9681
- return bytes;
10035
+ mkdirSync5(dirname4(path2), { recursive: true });
10036
+ writeFileSync5(path2, bytes);
10037
+ return bytes.length;
9682
10038
  })();
9683
10039
  this.inflight.set(path2, job);
9684
10040
  try {
@@ -9688,7 +10044,7 @@ class Finder {
9688
10044
  }
9689
10045
  }
9690
10046
  thumbs() {
9691
- while (this.thumbing < WORKERS && this.thumbQueue.length > 0) {
10047
+ while (this.thumbing < THUMB && this.thumbQueue.length > 0) {
9692
10048
  const { site, post: post2 } = this.thumbQueue.shift();
9693
10049
  this.thumbing++;
9694
10050
  this.thumb(site, post2).catch(() => {}).finally(() => {
@@ -9700,12 +10056,11 @@ class Finder {
9700
10056
  async thumb(site, post2) {
9701
10057
  const w = TILE.cols * this.cell.w;
9702
10058
  const h = TILE.rows * this.cell.h;
9703
- const path2 = join7(this.scratch, "tile", `${site.key}-${post2.id}-${w}x${h}.png`);
10059
+ const path2 = join7(cacheDir(site), "tile", `${post2.id}-${w}x${h}.png`);
9704
10060
  if (!existsSync3(path2)) {
9705
10061
  const thumb = join7(cacheDir(site), "thumb", `${post2.id}.${extension(post2.preview)}`);
9706
- const bytes = await this.cached(site, thumb, post2.preview);
9707
- mkdirSync4(dirname3(path2), { recursive: true });
9708
- writeFileSync4(path2, encodePng(contain(decodeImage(bytes, MAX_PIXELS), w, h)));
10062
+ await this.cached(site, thumb, post2.preview);
10063
+ await this.renders.run({ job: "thumb", from: thumb, to: path2, width: w, height: h });
9709
10064
  }
9710
10065
  this.thumbPath.set(this.mark(site, post2.id), path2);
9711
10066
  this.show();
@@ -9721,8 +10076,7 @@ class Finder {
9721
10076
  return;
9722
10077
  }
9723
10078
  if (this.current?.id === tile.id) {
9724
- this.present();
9725
- this.draw();
10079
+ this.reveal();
9726
10080
  return;
9727
10081
  }
9728
10082
  this.settle = setTimeout(() => void this.load(tile), SETTLE);
@@ -9744,9 +10098,9 @@ class Finder {
9744
10098
  view.fetching = { id: tile.id, got: 0, size: 0 };
9745
10099
  this.draw();
9746
10100
  }
9747
- const bytes = await this.cached(site, path2, version2.file, (got, size) => {
10101
+ const size = await this.cached(site, path2, version2.file, (got, total) => {
9748
10102
  if (view.fetching?.id === tile.id) {
9749
- view.fetching = { id: tile.id, got, size };
10103
+ view.fetching = { id: tile.id, got, size: total };
9750
10104
  this.draw();
9751
10105
  }
9752
10106
  });
@@ -9754,42 +10108,48 @@ class Finder {
9754
10108
  return;
9755
10109
  }
9756
10110
  view.fetching = undefined;
10111
+ if (site !== this.site) {
10112
+ return;
10113
+ }
10114
+ const current = { site, id: tile.id, path: path2, ext: version2.ext, size, using: "plain", failed: false };
9757
10115
  view.preparing = tile.id;
9758
10116
  this.flush();
9759
- await tick();
9760
- const image = decodeImage(bytes, MAX_PIXELS);
9761
- if (site !== this.site) {
10117
+ const plain2 = await this.render(current, "plain");
10118
+ if (control.signal.aborted || site !== this.site) {
9762
10119
  return;
9763
10120
  }
9764
- const current = {
9765
- site,
9766
- id: tile.id,
9767
- image,
9768
- plain: image,
9769
- failed: false,
9770
- bytes,
9771
- ext: version2.ext,
9772
- clear: transparency(image)
9773
- };
9774
- if (current.clear === 0 && canRemoveBackground() && this.setting("TTHEME_FIND_REMOVE_BG") !== "off") {
10121
+ if (plain2.clear === 0 && canRemoveBackground() && this.setting("TTHEME_FIND_REMOVE_BG") !== "off") {
9775
10122
  view.preparing = undefined;
9776
10123
  view.cutting = tile.id;
9777
10124
  this.flush();
9778
- const cut = await this.cutOut(site, tile.id, path2, control.signal);
10125
+ current.cut = await this.cutOut(site, tile.id, path2, control.signal);
9779
10126
  if (control.signal.aborted || site !== this.site) {
9780
10127
  return;
9781
10128
  }
9782
- if (cut) {
9783
- current.cut = cut;
9784
- current.image = cut;
9785
- current.clear = transparency(cut);
10129
+ view.cutting = undefined;
10130
+ if (current.cut) {
10131
+ view.preparing = tile.id;
10132
+ this.flush();
10133
+ const made = await this.render(current, "cut").catch(() => {
10134
+ return;
10135
+ });
10136
+ if (control.signal.aborted || site !== this.site) {
10137
+ return;
10138
+ }
10139
+ if (made && keepable(made.clear)) {
10140
+ current.using = "cut";
10141
+ } else {
10142
+ rmSync5(current.cut, { force: true });
10143
+ current.cut = undefined;
10144
+ current.failed = true;
10145
+ }
9786
10146
  } else {
9787
10147
  current.failed = true;
9788
10148
  }
9789
10149
  }
9790
10150
  this.current = current;
9791
10151
  if (view.tiles[view.focus]?.id === tile.id) {
9792
- this.present();
10152
+ await this.present();
9793
10153
  }
9794
10154
  this.preload();
9795
10155
  } catch (error) {
@@ -9810,13 +10170,16 @@ class Finder {
9810
10170
  }
9811
10171
  }
9812
10172
  async cutOut(site, id, path2, signal) {
9813
- const out = join7(this.scratch, "cut", `${site.key}-${id}.png`);
10173
+ const out = join7(cacheDir(site), "cut", `${id}.png`);
10174
+ if (existsSync3(out)) {
10175
+ return out;
10176
+ }
9814
10177
  try {
9815
- mkdirSync4(dirname3(out), { recursive: true });
9816
- const cut = existsSync3(out) ? decodeImage(new Uint8Array(readFileSync5(out)), MAX_PIXELS) : await removeBackground(path2, out, signal);
9817
- return keepable(transparency(cut)) ? cut : undefined;
10178
+ mkdirSync5(dirname4(out), { recursive: true });
10179
+ await removeBackground(path2, out, signal);
10180
+ return out;
9818
10181
  } catch {
9819
- rmSync4(out, { force: true });
10182
+ rmSync5(out, { force: true });
9820
10183
  return;
9821
10184
  }
9822
10185
  }
@@ -9825,10 +10188,8 @@ class Finder {
9825
10188
  if (!current?.cut || this.view.shown?.id !== current.id) {
9826
10189
  return;
9827
10190
  }
9828
- current.image = current.image === current.cut ? current.plain : current.cut;
9829
- current.clear = transparency(current.image);
9830
- this.present();
9831
- this.draw();
10191
+ current.using = current.using === "cut" ? "plain" : "cut";
10192
+ this.reveal();
9832
10193
  }
9833
10194
  preload() {
9834
10195
  const view = this.view;
@@ -9847,30 +10208,65 @@ class Finder {
9847
10208
  continue;
9848
10209
  }
9849
10210
  const path2 = this.origPath(site, post2.id, version2.ext);
9850
- if (existsSync3(path2)) {
9851
- continue;
9852
- }
9853
10211
  this.prefetching++;
9854
- this.cached(site, path2, version2.file).catch(() => {}).finally(() => {
10212
+ this.cached(site, path2, version2.file).then(() => this.render({ site, id: post2.id, path: path2, ext: version2.ext, size: 0, using: "plain", failed: false }, "plain")).catch(() => {}).finally(() => {
9855
10213
  this.prefetching--;
9856
10214
  });
9857
10215
  }
9858
10216
  }
9859
- present() {
9860
- const current = this.current;
9861
- if (!current) {
9862
- return;
9863
- }
10217
+ async render(current, using) {
9864
10218
  const W = this.cols * this.cell.w;
9865
10219
  const H = this.rows * this.cell.h;
9866
10220
  const width2 = Math.min(W, TRY_WIDTH);
9867
10221
  const height = Math.max(1, Math.round(H * width2 / W));
9868
- const cut = current.cut ? current.image === current.cut ? "on" : "off" : current.failed ? "failed" : "none";
9869
- const path2 = join7(this.scratch, `${current.id}${cut === "on" ? "c" : ""}-${width2}x${height}.png`);
9870
- if (!existsSync3(path2)) {
9871
- writeFileSync4(path2, encodePng(tryOn(current.image, this.entry, this.tone, width2, height)));
10222
+ const path2 = join7(this.scratch, `${current.site.key}-${current.id}${using === "cut" ? "c" : ""}-${width2}x${height}.png`);
10223
+ const key = `${current.site.key}:${current.id}:${using}`;
10224
+ const known = this.clarity.get(key);
10225
+ if (known !== undefined && existsSync3(path2)) {
10226
+ return { clear: known, path: path2 };
10227
+ }
10228
+ const clear = await this.renders.run({
10229
+ job: "show",
10230
+ from: using === "cut" ? current.cut : current.path,
10231
+ to: path2,
10232
+ width: width2,
10233
+ height,
10234
+ colors: this.entry,
10235
+ tone: this.tone
10236
+ });
10237
+ this.clarity.set(key, clear);
10238
+ return { clear, path: path2 };
10239
+ }
10240
+ async present() {
10241
+ const current = this.current;
10242
+ if (!current) {
10243
+ return;
10244
+ }
10245
+ const using = current.using;
10246
+ const { clear, path: path2 } = await this.render(current, using);
10247
+ if (this.current !== current || current.using !== using) {
10248
+ return;
9872
10249
  }
9873
- this.view.shown = { id: current.id, clear: current.clear, bytes: current.bytes.length, path: path2, cut };
10250
+ this.view.shown = {
10251
+ id: current.id,
10252
+ clear,
10253
+ bytes: current.size,
10254
+ path: path2,
10255
+ cut: current.cut ? using === "cut" ? "on" : "off" : current.failed ? "failed" : "none"
10256
+ };
10257
+ }
10258
+ reveal() {
10259
+ const view = this.view;
10260
+ view.preparing = this.current?.id;
10261
+ this.flush();
10262
+ this.present().catch((error) => {
10263
+ view.error = error instanceof Error ? error.message : String(error);
10264
+ }).finally(() => {
10265
+ if (view.preparing === this.current?.id) {
10266
+ view.preparing = undefined;
10267
+ }
10268
+ this.draw();
10269
+ });
9874
10270
  }
9875
10271
  async install() {
9876
10272
  const view = this.view;
@@ -9882,15 +10278,24 @@ class Finder {
9882
10278
  view.installing = tile.id;
9883
10279
  view.error = undefined;
9884
10280
  this.flush();
9885
- await tick();
9886
10281
  try {
9887
- installBackdrop(this.home, this.entry, this.tone, current.image, {
9888
- site: current.site.key,
9889
- id: current.id,
9890
- ext: current.ext,
9891
- bytes: current.bytes,
9892
- from: `${current.site.name} ${current.id} ${current.site.pageUrl(current.id)}`
10282
+ await this.renders.run({
10283
+ job: "backdrop",
10284
+ from: current.using === "cut" ? current.cut : current.path,
10285
+ source: current.path,
10286
+ home: this.home,
10287
+ colors: this.entry,
10288
+ tone: this.tone,
10289
+ origin: {
10290
+ site: current.site.key,
10291
+ id: current.id,
10292
+ ext: current.ext,
10293
+ from: `${current.site.name} ${current.id} ${current.site.pageUrl(current.id)}`
10294
+ }
9893
10295
  });
10296
+ const known = readCache(current.site, "owners.json");
10297
+ known[current.id] = this.posts.get(this.mark(current.site, current.id))?.owner ?? "";
10298
+ writeCache(current.site, "owners.json", known);
9894
10299
  process.stderr.write(`background · ${this.entry.name} ← ${current.site.name} ${current.id}
9895
10300
  `);
9896
10301
  this.finish(0);
@@ -9981,19 +10386,38 @@ async function runFind(name) {
9981
10386
  return new Finder(home, catalog, entry, entry.booru ?? "").run();
9982
10387
  }
9983
10388
 
10389
+ // src/images.ts
10390
+ function runImage(name, action) {
10391
+ const home = configHome();
10392
+ find2(readCatalog(home).palettes, name);
10393
+ if (action === "next" || action === "prev") {
10394
+ const { key, at: at2, of } = switchImage(home, name, action === "next" ? 1 : -1);
10395
+ process.stderr.write(`background · ${name} ${at2}/${of} ${key}
10396
+ `);
10397
+ return 0;
10398
+ }
10399
+ if (action === "drop") {
10400
+ const { key, left } = dropImage(home, name);
10401
+ process.stderr.write(`background · ${name} removed ${key ?? "the picture"} · ${left} left
10402
+ `);
10403
+ return 0;
10404
+ }
10405
+ throw new Error(`unknown image action ${action} — next, prev or drop`);
10406
+ }
10407
+
9984
10408
  // src/init.ts
9985
10409
  import {
9986
10410
  chmodSync,
9987
10411
  copyFileSync,
9988
10412
  existsSync as existsSync5,
9989
- mkdirSync as mkdirSync5,
9990
- readdirSync as readdirSync3,
9991
- readFileSync as readFileSync6,
9992
- rmSync as rmSync5,
9993
- writeFileSync as writeFileSync5
10413
+ mkdirSync as mkdirSync6,
10414
+ readdirSync as readdirSync4,
10415
+ readFileSync as readFileSync7,
10416
+ rmSync as rmSync6,
10417
+ writeFileSync as writeFileSync6
9994
10418
  } from "node:fs";
9995
10419
  import { homedir as homedir3 } from "node:os";
9996
- import { dirname as dirname4, join as join8 } from "node:path";
10420
+ import { dirname as dirname5, join as join8 } from "node:path";
9997
10421
 
9998
10422
  // node_modules/@clack/core/dist/index.mjs
9999
10423
  import { styleText } from "node:util";
@@ -11932,12 +12356,12 @@ async function runBrowse() {
11932
12356
 
11933
12357
  // src/init.ts
11934
12358
  function copyDir(copies, from, to) {
11935
- for (const f2 of readdirSync3(from)) {
12359
+ for (const f2 of readdirSync4(from)) {
11936
12360
  copies.push({ from: join8(from, f2), to: join8(to, f2) });
11937
12361
  }
11938
12362
  }
11939
12363
  function loadManifest(root2) {
11940
- return JSON.parse(readFileSync6(join8(root2, "dist", "manifest.json"), "utf8"));
12364
+ return JSON.parse(readFileSync7(join8(root2, "dist", "manifest.json"), "utf8"));
11941
12365
  }
11942
12366
  function planInit(opts, paths) {
11943
12367
  const dist = join8(paths.root, "dist");
@@ -11953,7 +12377,7 @@ function planInit(opts, paths) {
11953
12377
  const configPath = join8(home, "config.zsh");
11954
12378
  const settings2 = {
11955
12379
  file: configPath,
11956
- content: configFile(existsSync5(configPath) ? readFileSync6(configPath, "utf8") : "")
12380
+ content: configFile(existsSync5(configPath) ? readFileSync7(configPath, "utf8") : "")
11957
12381
  };
11958
12382
  const notes = [];
11959
12383
  if (opts.terminals.includes("ghostty")) {
@@ -11961,7 +12385,7 @@ function planInit(opts, paths) {
11961
12385
  }
11962
12386
  if (opts.terminals.includes("alacritty")) {
11963
12387
  const config = join8(paths.configHome, "alacritty", "alacritty.toml");
11964
- if (existsSync5(config) && !readFileSync6(config, "utf8").includes("# ttheme begin")) {
12388
+ if (existsSync5(config) && !readFileSync7(config, "utf8").includes("# ttheme begin")) {
11965
12389
  notes.push("alacritty.toml already exists — ttheme left it alone; add its themes/ import yourself");
11966
12390
  }
11967
12391
  }
@@ -11971,23 +12395,23 @@ function planInit(opts, paths) {
11971
12395
  }
11972
12396
  function applyInit(plan) {
11973
12397
  for (const c2 of plan.copies) {
11974
- mkdirSync5(dirname4(c2.to), { recursive: true });
11975
- rmSync5(c2.to, { force: true });
12398
+ mkdirSync6(dirname5(c2.to), { recursive: true });
12399
+ rmSync6(c2.to, { force: true });
11976
12400
  copyFileSync(c2.from, c2.to);
11977
12401
  if (c2.executable) {
11978
12402
  chmodSync(c2.to, 493);
11979
12403
  }
11980
12404
  }
11981
- mkdirSync5(dirname4(plan.settings.file), { recursive: true });
11982
- writeFileSync5(plan.settings.file, plan.settings.content);
11983
- const configHome2 = dirname4(dirname4(plan.settings.file));
12405
+ mkdirSync6(dirname5(plan.settings.file), { recursive: true });
12406
+ writeFileSync6(plan.settings.file, plan.settings.content);
12407
+ const configHome2 = dirname5(dirname5(plan.settings.file));
11984
12408
  writeCatalog(configHome2, plan.catalog);
11985
12409
  writeInstalled(configHome2, plan.installed);
11986
12410
  sync(configHome2, plan.catalog, plan.installed);
11987
12411
  for (const e of plan.edits) {
11988
- mkdirSync5(dirname4(e.file), { recursive: true });
11989
- const current = existsSync5(e.file) ? readFileSync6(e.file, "utf8") : "";
11990
- writeFileSync5(e.file, upsertBlock(current, e.block));
12412
+ mkdirSync6(dirname5(e.file), { recursive: true });
12413
+ const current = existsSync5(e.file) ? readFileSync7(e.file, "utf8") : "";
12414
+ writeFileSync6(e.file, upsertBlock(current, e.block));
11991
12415
  }
11992
12416
  }
11993
12417
  function accepted(value) {
@@ -12009,7 +12433,7 @@ function verify(plan) {
12009
12433
  const missing = [
12010
12434
  ...plan.copies.filter((c2) => !existsSync5(c2.to)).map((c2) => c2.to),
12011
12435
  ...existsSync5(plan.settings.file) ? [] : [plan.settings.file],
12012
- ...plan.edits.filter((e) => !readFileSync6(e.file, "utf8").includes("# ttheme begin")).map((e) => e.file)
12436
+ ...plan.edits.filter((e) => !readFileSync7(e.file, "utf8").includes("# ttheme begin")).map((e) => e.file)
12013
12437
  ];
12014
12438
  if (missing.length > 0) {
12015
12439
  throw new Error(`init left gaps:
@@ -12134,6 +12558,9 @@ function createProgram() {
12134
12558
  program2.command("find", { hidden: true }).argument("<palette>").description("pick a safebooru background for a palette — preview opens this on tab").action(async (name) => {
12135
12559
  process.exitCode = await runFind(name);
12136
12560
  });
12561
+ program2.command("image", { hidden: true }).argument("<palette>").argument("<action>", "next, prev or drop").description("switch a palette between its saved backgrounds, or remove the one shown — preview calls this").action((name, action) => {
12562
+ process.exitCode = runImage(name, action);
12563
+ });
12137
12564
  program2.command("update").description("refresh the catalog from the registry").action(() => runUpdate());
12138
12565
  return program2;
12139
12566
  }
@@ -12152,4 +12579,6 @@ ${error instanceof Error ? error.message : String(error)}`);
12152
12579
  }
12153
12580
 
12154
12581
  // src/bin.ts
12155
- process.exitCode = await runCli(process.argv);
12582
+ if (isMainThread2) {
12583
+ process.exitCode = await runCli(process.argv);
12584
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.3",
2
+ "version": "0.1.4",
3
3
  "gate": [
4
4
  {
5
5
  "rule": "foreground",
@@ -1,7 +1,7 @@
1
1
  # Generated by `mise run build` — do not edit.
2
2
  # Source of truth: themes/*.toml in the ttheme repo.
3
3
 
4
- typeset -g TTHEME_VERSION=0.1.3
4
+ typeset -g TTHEME_VERSION=0.1.4
5
5
 
6
6
  # seed for `ttheme config` when no config.zsh exists yet
7
7
  typeset -g TTHEME_CONFIG_TEMPLATE='# ttheme settings — exported variables win over this file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kecan0406/ttheme",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ttheme": "bin/ttheme.js"
@@ -30,6 +30,10 @@ __tt_pv_bg_findable() { return 1 }
30
30
 
31
31
  __tt_pv_bg_find() { return 1 }
32
32
 
33
+ __tt_pv_bg_images() { REPLY=1 }
34
+
35
+ __tt_pv_bg_image() { : }
36
+
33
37
  __tt_pv_bg_panel() { : }
34
38
 
35
39
  __tt_pv_bg_close() { : }
@@ -420,6 +420,32 @@ __tt_pv_bg_find() {
420
420
  return 0
421
421
  }
422
422
 
423
+ __tt_pv_bg_images() {
424
+ local -a shelf=(${TTHEME_CONFIG:h}/backgrounds/shelf/$1/*(N/))
425
+ REPLY=$(( ${#shelf} + 1 ))
426
+ }
427
+
428
+ __tt_pv_bg_image() {
429
+ local name=$tune act=$1 err REPLY
430
+ local -i rc
431
+ if [[ $act != drop ]]; then
432
+ __tt_pv_bg_images $name
433
+ (( REPLY > 1 )) || { msg="$name has one image" msgt=200; return 0 }
434
+ fi
435
+ __tt_pv_untune
436
+ __tt_pv_bg_close
437
+ err=$(__tt_cli image $name $act 2>&1)
438
+ rc=$?
439
+ err=${${err//$'\n'/ }## #}
440
+ resized=1 bgname="" bgshown="" bgsent=() bgdim=()
441
+ unset "bgsrc[$name]"
442
+ __tt_bg_load $name
443
+ bgload[$name]="" bgedit[$name]=1
444
+ msg=${err:-"background · $name"} msgt=$(( rc ? 300 : 200 ))
445
+ [[ -r ${TTHEME_CONFIG:h}/backgrounds/$name.conf ]] || return 0
446
+ tune=$name tf=1 tsnap="${bgsize[$name]} ${bgpos[$name]} ${bgop[$name]} ${bgoff[$name]}"
447
+ }
448
+
423
449
  __tt_pv_bg_panel() {
424
450
  local name=$1 z=$'\e[0m' b=$'\e[1m' d=$'\e[2m' c=$ac val sty REPLY
425
451
  local -a labs=(size position opacity) at=(0 3 6)
package/shell/ttheme.zsh CHANGED
@@ -618,6 +618,8 @@ __tt_pv_foot() {
618
618
  kk+=(space '=' enter)
619
619
  if (( bgoff[$tune] )); then kl+=(show default keep); else kl+=(hide default keep); fi
620
620
  __tt_pv_bg_findable $tune && { kk+=(f); kl+=(find) }
621
+ __tt_pv_bg_images $tune
622
+ (( REPLY > 1 )) && { kk+=(', .' D); kl+=("image ×$REPLY" remove) }
621
623
  right=$b"esc"$z$d" undo"$z
622
624
  elif (( conf )); then
623
625
  badge=CONFIG kk=(↑↓ ←→ enter) kl=(setting value save)
@@ -700,8 +702,8 @@ __tt_pv_help() {
700
702
  "enter · esc restores $back"
701
703
  )
702
704
  if (( bgcw )); then
703
- hk+=("tune bg" "" "" "")
704
- hv+=("tab · finds one on the boorus if none" "↑↓ field ←→ step ⇧←→ ×10 1-9 place" "space hides · = default · f replaces" "enter keeps · esc undoes")
705
+ hk+=("tune bg" "" "" "" "")
706
+ hv+=("tab · finds one on the boorus if none" "↑↓ field ←→ step ⇧←→ ×10 1-9 place" "space hides · = default · f adds one" "enter keeps · esc undoes" ", . other saved images · D removes this one")
705
707
  fi
706
708
  hk+=(config "")
707
709
  hv+=("alt-c · ↑↓ setting ←→ value" "enter saves · esc undoes")
@@ -782,6 +784,9 @@ __tt_pv_tune() {
782
784
  ;;
783
785
  ' ') __tt_pv_bg_adjust on ;;
784
786
  '=') __tt_pv_bg_adjust def ;;
787
+ ',') __tt_pv_bg_image prev ;;
788
+ '.') __tt_pv_bg_image next ;;
789
+ D) __tt_pv_bg_image drop ;;
785
790
  f)
786
791
  __tt_pv_bg_findable $tune || return 0
787
792
  name=$tune