@kecan0406/ttheme 0.1.2 → 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/README.md +77 -20
- package/bin/ttheme.js +1883 -311
- package/dist/manifest.json +1 -1
- package/dist/shell/palettes.zsh +29 -103
- package/package.json +1 -1
- package/shell/adapters/_osc.zsh +4 -0
- package/shell/adapters/ghostty.zsh +27 -1
- package/shell/ttheme.zsh +13 -3
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.
|
|
6150
|
+
version: "0.1.4",
|
|
6148
6151
|
type: "module",
|
|
6149
6152
|
bin: {
|
|
6150
6153
|
ttheme: "bin/ttheme.js"
|
|
@@ -6804,6 +6807,42 @@ var CONFIG_SETTINGS = {
|
|
|
6804
6807
|
TTHEME_SORT: {
|
|
6805
6808
|
doc: "# series and palettes in ttheme and preview: abc sorts them by name, series keeps the order they were added (default abc)",
|
|
6806
6809
|
default: "abc"
|
|
6810
|
+
},
|
|
6811
|
+
TTHEME_FIND_RATING: {
|
|
6812
|
+
doc: "# the ratings find lists, any of safe, questionable and explicit, each booru read in its own rating vocabulary (default safe)",
|
|
6813
|
+
default: "safe"
|
|
6814
|
+
},
|
|
6815
|
+
TTHEME_FIND_BLOCK: {
|
|
6816
|
+
doc: '# the posts find drops by tag: nudity, underwear, both, or none to keep them all (default "nudity underwear")',
|
|
6817
|
+
default: "nudity underwear"
|
|
6818
|
+
},
|
|
6819
|
+
TTHEME_FIND_POSTS: {
|
|
6820
|
+
doc: "# what find lists first: all is every post of the character, cutouts are the transparent ones (default all)",
|
|
6821
|
+
default: "all"
|
|
6822
|
+
},
|
|
6823
|
+
TTHEME_FIND_CUTOUTS: {
|
|
6824
|
+
doc: '# the tags find calls a transparent cutout, per site as key=tag,tag pairs — e.g. "safebooru=transparent_background yande=transparent_png,vector" (default the built-in tags)',
|
|
6825
|
+
default: ""
|
|
6826
|
+
},
|
|
6827
|
+
TTHEME_FIND_ORDER: {
|
|
6828
|
+
doc: "# the order find lists posts in: newest or score (default newest)",
|
|
6829
|
+
default: "newest"
|
|
6830
|
+
},
|
|
6831
|
+
TTHEME_FIND_SETS: {
|
|
6832
|
+
doc: "# runs of the same picture at the same size from one uploader: fold shows them as one tile, show lists each (default fold)",
|
|
6833
|
+
default: "fold"
|
|
6834
|
+
},
|
|
6835
|
+
TTHEME_FIND_REMOVE_BG: {
|
|
6836
|
+
doc: "# an opaque picture tried on in find: on cuts the character out with macOS Vision, off leaves it as it is (default on)",
|
|
6837
|
+
default: "on"
|
|
6838
|
+
},
|
|
6839
|
+
TTHEME_FIND_UNBLOCK: {
|
|
6840
|
+
doc: "# when a network blocks a booru by name, 1 sends find through a local proxy that splits the TLS handshake (default 0)",
|
|
6841
|
+
default: "0"
|
|
6842
|
+
},
|
|
6843
|
+
TTHEME_FIND_HOSTS: {
|
|
6844
|
+
doc: '# send a find site somewhere else, as key=https://host pairs — e.g. "konachan=https://konachan.com danbooru=https://danbooru.donmai.us" (default none)',
|
|
6845
|
+
default: ""
|
|
6807
6846
|
}
|
|
6808
6847
|
};
|
|
6809
6848
|
function settingLine(name, value) {
|
|
@@ -6831,6 +6870,17 @@ ${settingLine(name, s.default)}`);
|
|
|
6831
6870
|
`)}
|
|
6832
6871
|
`;
|
|
6833
6872
|
}
|
|
6873
|
+
function withSetting(content, name, value) {
|
|
6874
|
+
const line = settingLine(name, value);
|
|
6875
|
+
const pattern = settingPattern(name);
|
|
6876
|
+
if (pattern.test(content)) {
|
|
6877
|
+
return content.replace(pattern, line);
|
|
6878
|
+
}
|
|
6879
|
+
return `${content.replace(/\n*$/, `
|
|
6880
|
+
`)}
|
|
6881
|
+
${line}
|
|
6882
|
+
`;
|
|
6883
|
+
}
|
|
6834
6884
|
function configFile(content) {
|
|
6835
6885
|
if (content === "") {
|
|
6836
6886
|
return configTemplate();
|
|
@@ -6848,7 +6898,7 @@ import = ["${themePath}"]` : "";
|
|
|
6848
6898
|
// src/emit/shell.ts
|
|
6849
6899
|
function palettesZsh(palettes) {
|
|
6850
6900
|
for (const p of palettes) {
|
|
6851
|
-
for (const field of [p.name, p.group, p.native ?? "", p.ansiSource
|
|
6901
|
+
for (const field of [p.name, p.group, p.native ?? "", p.ansiSource]) {
|
|
6852
6902
|
if (/["$`\\]/.test(field)) {
|
|
6853
6903
|
throw new Error(`${p.name}: "${field}" contains a character that breaks zsh quoting`);
|
|
6854
6904
|
}
|
|
@@ -6890,11 +6940,6 @@ function palettesZsh(palettes) {
|
|
|
6890
6940
|
"typeset -gA TTHEME_SRC=(",
|
|
6891
6941
|
...palettes.map((p) => entry(p, p.ansiSource)),
|
|
6892
6942
|
")",
|
|
6893
|
-
"",
|
|
6894
|
-
"# the safebooru tag preview searches when a palette has no background yet",
|
|
6895
|
-
"typeset -gA TTHEME_BOORU=(",
|
|
6896
|
-
...palettes.filter((p) => p.booru).map((p) => entry(p, p.booru ?? "")),
|
|
6897
|
-
")",
|
|
6898
6943
|
""
|
|
6899
6944
|
].join(`
|
|
6900
6945
|
`);
|
|
@@ -6999,12 +7044,13 @@ ${themes.length} themes (${rotation(themes).length} in the new-tab rotation) ->
|
|
|
6999
7044
|
}
|
|
7000
7045
|
|
|
7001
7046
|
// src/find.ts
|
|
7002
|
-
import {
|
|
7047
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
7048
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7003
7049
|
import { tmpdir } from "node:os";
|
|
7004
|
-
import { dirname as
|
|
7050
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
7005
7051
|
|
|
7006
7052
|
// src/backdrop.ts
|
|
7007
|
-
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";
|
|
7008
7054
|
import { join as join3 } from "node:path";
|
|
7009
7055
|
|
|
7010
7056
|
// src/png.ts
|
|
@@ -7023,12 +7069,21 @@ function decodePng(bytes) {
|
|
|
7023
7069
|
const png = import_pngjs.PNG.sync.read(Buffer.from(bytes));
|
|
7024
7070
|
return { width: png.width, height: png.height, data: new Uint8Array(png.data) };
|
|
7025
7071
|
}
|
|
7026
|
-
function decodeImage(bytes) {
|
|
7072
|
+
function decodeImage(bytes, limit) {
|
|
7027
7073
|
if (isPng(bytes)) {
|
|
7074
|
+
const head = pngHead(bytes);
|
|
7075
|
+
if (head && head.width * head.height > limit) {
|
|
7076
|
+
throw new Error(`${head.width}×${head.height} is over ${limit / 1e6} megapixels`);
|
|
7077
|
+
}
|
|
7028
7078
|
return decodePng(bytes);
|
|
7029
7079
|
}
|
|
7030
7080
|
if (bytes[0] === 255 && bytes[1] === 216) {
|
|
7031
|
-
const jpeg = import_jpeg_js.decode(bytes, {
|
|
7081
|
+
const jpeg = import_jpeg_js.decode(bytes, {
|
|
7082
|
+
useTArray: true,
|
|
7083
|
+
formatAsRGBA: true,
|
|
7084
|
+
maxResolutionInMP: limit / 1e6,
|
|
7085
|
+
maxMemoryUsageInMB: 1024
|
|
7086
|
+
});
|
|
7032
7087
|
return { width: jpeg.width, height: jpeg.height, data: jpeg.data };
|
|
7033
7088
|
}
|
|
7034
7089
|
throw new Error("not a PNG or JPEG image");
|
|
@@ -7060,9 +7115,8 @@ function pngHead(bytes) {
|
|
|
7060
7115
|
if (bytes[25] === 4 || bytes[25] === 6) {
|
|
7061
7116
|
return { width, height, alpha: true };
|
|
7062
7117
|
}
|
|
7063
|
-
const trns = find(bytes, "tRNS");
|
|
7064
7118
|
const idat = find(bytes, "IDAT");
|
|
7065
|
-
return { width, height, alpha:
|
|
7119
|
+
return { width, height, alpha: find(idat === -1 ? bytes : bytes.subarray(0, idat), "tRNS") !== -1 };
|
|
7066
7120
|
}
|
|
7067
7121
|
function alphaBox(image, threshold = CLEAR) {
|
|
7068
7122
|
let x0 = image.width;
|
|
@@ -7180,7 +7234,11 @@ var FILL = { width: 2560, height: 1550 };
|
|
|
7180
7234
|
var FIGURE = 2560;
|
|
7181
7235
|
var FAINT = 0.1;
|
|
7182
7236
|
var HEADROOM = 0.15;
|
|
7237
|
+
var KEEP = 0.55;
|
|
7238
|
+
var PLACE = 0.65;
|
|
7239
|
+
var STANDS = 3;
|
|
7183
7240
|
var PEAK = luminance(mix("#19161e", "#9b86c8", 0.2));
|
|
7241
|
+
var SHELF = "shelf";
|
|
7184
7242
|
function mix(a, b, t) {
|
|
7185
7243
|
const from = rgb(a);
|
|
7186
7244
|
const to = rgb(b);
|
|
@@ -7270,11 +7328,14 @@ function figureBox(image) {
|
|
|
7270
7328
|
function headAnchor(box) {
|
|
7271
7329
|
return Math.min(0.5, (HEADROOM + 0.5) * (box.w * FILL.height / FILL.width / box.h));
|
|
7272
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
|
+
}
|
|
7273
7335
|
function fillBox(box, width, height, anchor) {
|
|
7274
7336
|
const h = box.w * height / width;
|
|
7275
7337
|
if (h <= box.h) {
|
|
7276
|
-
|
|
7277
|
-
return { x: box.x, y: box.y + top, w: box.w, h };
|
|
7338
|
+
return band(box, h, anchor);
|
|
7278
7339
|
}
|
|
7279
7340
|
const w = box.h * width / height;
|
|
7280
7341
|
return { x: box.x + (box.w - w) / 2, y: box.y, w, h: box.h };
|
|
@@ -7291,20 +7352,66 @@ function figure(image, box) {
|
|
|
7291
7352
|
const k = Math.min(1, FIGURE / Math.max(box.w, box.h));
|
|
7292
7353
|
return resample(image, box, Math.round(box.w * k), Math.round(box.h * k));
|
|
7293
7354
|
}
|
|
7294
|
-
function
|
|
7295
|
-
|
|
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);
|
|
7296
7391
|
}
|
|
7297
|
-
function tryOn(image, colors, tone, width, height) {
|
|
7392
|
+
function tryOn(image, colors, tone, width, height, clear) {
|
|
7298
7393
|
const box = figureBox(image);
|
|
7299
|
-
const
|
|
7300
|
-
|
|
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);
|
|
7301
7407
|
}
|
|
7302
7408
|
function backgroundsDir(configHome) {
|
|
7303
7409
|
return join3(configHome, "ttheme", "backgrounds");
|
|
7304
7410
|
}
|
|
7305
|
-
function backdropConf(name, fillPath, opacity, from) {
|
|
7411
|
+
function backdropConf(name, fillPath, opacity, from, key) {
|
|
7306
7412
|
return [
|
|
7307
7413
|
...from ? [`# from ${from}`] : [],
|
|
7414
|
+
...key ? [`# image ${key}`] : [],
|
|
7308
7415
|
`background-image = ${fillPath}`,
|
|
7309
7416
|
"background-image-fit = cover",
|
|
7310
7417
|
"background-image-position = center",
|
|
@@ -7322,26 +7429,139 @@ function clearBackdrop(dir, name) {
|
|
|
7322
7429
|
}
|
|
7323
7430
|
}
|
|
7324
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
|
+
}
|
|
7325
7543
|
function installBackdrop(configHome, colors, tone, image, original) {
|
|
7326
7544
|
const dir = backgroundsDir(configHome);
|
|
7327
7545
|
const name = colors.name;
|
|
7546
|
+
const key = imageKey(original);
|
|
7328
7547
|
mkdirSync(join3(dir, "originals"), { recursive: true });
|
|
7329
|
-
|
|
7548
|
+
retire(dir, name, key);
|
|
7330
7549
|
const box = figureBox(image);
|
|
7331
7550
|
const anchor = headAnchor(box);
|
|
7551
|
+
const clear = transparency(image);
|
|
7332
7552
|
const whole = join3(dir, `${name}.png`);
|
|
7333
7553
|
const fillPath = join3(dir, `${name}@fill-${Math.round(anchor * 100)}.png`);
|
|
7334
7554
|
const conf = join3(dir, `${name}.conf`);
|
|
7335
7555
|
const source = join3(dir, "originals", `${name}-${original.site}_${original.id}.${original.ext}`);
|
|
7336
7556
|
writeFileSync(whole, encodePng(tint(figure(image, box), colors.background, tone.color)));
|
|
7337
|
-
writeFileSync(fillPath, encodePng(tint(fill(image, box, anchor), colors.background, tone.color)));
|
|
7338
|
-
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));
|
|
7339
7559
|
writeFileSync(source, original.bytes);
|
|
7340
7560
|
return [whole, fillPath, conf, source];
|
|
7341
7561
|
}
|
|
7342
7562
|
function origins(configHome) {
|
|
7343
7563
|
const dir = join3(backgroundsDir(configHome), "originals");
|
|
7344
|
-
const
|
|
7564
|
+
const newest2 = new Map;
|
|
7345
7565
|
let files;
|
|
7346
7566
|
try {
|
|
7347
7567
|
files = readdirSync2(dir);
|
|
@@ -7355,23 +7575,49 @@ function origins(configHome) {
|
|
|
7355
7575
|
continue;
|
|
7356
7576
|
}
|
|
7357
7577
|
const at2 = statSync(join3(dir, file)).mtimeMs;
|
|
7358
|
-
const known =
|
|
7578
|
+
const known = newest2.get(name);
|
|
7359
7579
|
if (!known || at2 > known.at) {
|
|
7360
|
-
|
|
7580
|
+
newest2.set(name, { site, id: Number(id), at: at2 });
|
|
7361
7581
|
}
|
|
7362
7582
|
}
|
|
7363
|
-
return new Map([...
|
|
7583
|
+
return new Map([...newest2].map(([name, { site, id }]) => [name, { site, id }]));
|
|
7364
7584
|
}
|
|
7365
7585
|
|
|
7366
7586
|
// src/booru.ts
|
|
7587
|
+
import { readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
|
|
7588
|
+
import { setDefaultAutoSelectFamilyAttemptTimeout } from "node:net";
|
|
7367
7589
|
import { homedir } from "node:os";
|
|
7368
7590
|
import { join as join4 } from "node:path";
|
|
7591
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
7369
7592
|
var PAGE = 100;
|
|
7370
|
-
var
|
|
7593
|
+
var MAX_PIXELS = 25000000;
|
|
7594
|
+
var CACHE_BYTES = 512 * 1024 * 1024;
|
|
7595
|
+
var RATINGS = ["safe", "questionable", "explicit"];
|
|
7596
|
+
var BLOCKS = ["nudity", "underwear"];
|
|
7597
|
+
var EXPOSED = {
|
|
7598
|
+
nudity: new Set(["nude", "naked", "topless", "bottomless", "nipples", "naked_towel", "undressing"]),
|
|
7599
|
+
underwear: new Set(["underwear", "panties", "pantsu", "bra", "lingerie", "pantyshot"])
|
|
7600
|
+
};
|
|
7601
|
+
var MOEBOORU = { safe: "s", questionable: "q", explicit: "e" };
|
|
7371
7602
|
var MIRRORS = new Set(["danbooru", "gelbooru", "konachan", "yande.re", "sankaku"]);
|
|
7372
7603
|
var HEAD = 8191;
|
|
7373
7604
|
var TIMEOUT = 20000;
|
|
7605
|
+
var CONNECT = 3000;
|
|
7606
|
+
var MAX_WAIT = 60000;
|
|
7607
|
+
var TRIES = 3;
|
|
7374
7608
|
var AGENT = `ttheme/${package_default.version} (+${package_default.homepage})`;
|
|
7609
|
+
var HELD = ["orig", "tile", "thumb", "cut"];
|
|
7610
|
+
var paused = new Map;
|
|
7611
|
+
var HOSTS = hostMap(process.env.TTHEME_FIND_HOSTS);
|
|
7612
|
+
var CUTOUTS = cutoutMap(process.env.TTHEME_FIND_CUTOUTS);
|
|
7613
|
+
setDefaultAutoSelectFamilyAttemptTimeout(CONNECT);
|
|
7614
|
+
function listing(dir) {
|
|
7615
|
+
try {
|
|
7616
|
+
return readdirSync3(dir);
|
|
7617
|
+
} catch {
|
|
7618
|
+
return [];
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7375
7621
|
function absolute(url) {
|
|
7376
7622
|
return url.startsWith("//") ? `https:${url}` : url;
|
|
7377
7623
|
}
|
|
@@ -7384,26 +7630,45 @@ function records(text) {
|
|
|
7384
7630
|
return [];
|
|
7385
7631
|
}
|
|
7386
7632
|
const raw = JSON.parse(body);
|
|
7387
|
-
|
|
7633
|
+
if (Array.isArray(raw)) {
|
|
7634
|
+
return raw;
|
|
7635
|
+
}
|
|
7636
|
+
const posts = raw.posts;
|
|
7637
|
+
return Array.isArray(posts) ? posts : [];
|
|
7638
|
+
}
|
|
7639
|
+
function tagTypes(text) {
|
|
7640
|
+
const body = text.trim();
|
|
7641
|
+
const raw = body ? JSON.parse(body) : {};
|
|
7642
|
+
const tags = raw.tags ?? {};
|
|
7643
|
+
return new Map(Object.entries(tags).map(([name, type]) => [name, String(type)]));
|
|
7388
7644
|
}
|
|
7389
|
-
function
|
|
7390
|
-
const
|
|
7391
|
-
|
|
7645
|
+
function version(url, width, height) {
|
|
7646
|
+
const file = absolute(String(url ?? ""));
|
|
7647
|
+
return { file, width: Number(width) || 0, height: Number(height) || 0, ext: extension(file) };
|
|
7648
|
+
}
|
|
7649
|
+
function post(p, raw) {
|
|
7650
|
+
const preview = absolute(String(raw.preview ?? ""));
|
|
7651
|
+
if (!raw.file || !preview || !Number(p.id)) {
|
|
7392
7652
|
return [];
|
|
7393
7653
|
}
|
|
7654
|
+
const width = Number(p.width ?? p.image_width) || 0;
|
|
7655
|
+
const height = Number(p.height ?? p.image_height) || 0;
|
|
7394
7656
|
return [
|
|
7395
7657
|
{
|
|
7396
7658
|
id: Number(p.id),
|
|
7397
|
-
width
|
|
7398
|
-
height
|
|
7399
|
-
file,
|
|
7659
|
+
width,
|
|
7660
|
+
height,
|
|
7661
|
+
file: raw.file,
|
|
7400
7662
|
preview,
|
|
7401
|
-
ext,
|
|
7402
|
-
owner: String(owner ?? ""),
|
|
7663
|
+
ext: raw.ext,
|
|
7664
|
+
owner: String(raw.owner ?? ""),
|
|
7665
|
+
artist: raw.artist,
|
|
7666
|
+
score: Number(p.score) || 0,
|
|
7403
7667
|
rating: String(p.rating ?? ""),
|
|
7404
|
-
md5: String(md5 ?? ""),
|
|
7668
|
+
md5: String(raw.md5 ?? ""),
|
|
7405
7669
|
source: String(p.source ?? ""),
|
|
7406
|
-
tags:
|
|
7670
|
+
tags: raw.tags.split(/\s+/).filter(Boolean),
|
|
7671
|
+
smaller: raw.versions.filter((v) => v.file && v.width > 0 && v.height > 0 && v.width * v.height < width * height)
|
|
7407
7672
|
}
|
|
7408
7673
|
];
|
|
7409
7674
|
}
|
|
@@ -7411,54 +7676,239 @@ function parseGelbooru(text, origin = "https://safebooru.org") {
|
|
|
7411
7676
|
return records(text).flatMap((p) => {
|
|
7412
7677
|
const stored = p.directory && p.image ? `${origin}/images/${p.directory}/${p.image}` : "";
|
|
7413
7678
|
const file = absolute(String(p.file_url || stored));
|
|
7414
|
-
return post(p,
|
|
7679
|
+
return post(p, {
|
|
7680
|
+
preview: p.preview_url,
|
|
7681
|
+
file,
|
|
7682
|
+
ext: extension(file),
|
|
7683
|
+
owner: p.owner,
|
|
7684
|
+
artist: "",
|
|
7685
|
+
md5: p.hash ?? p.md5,
|
|
7686
|
+
tags: String(p.tags ?? ""),
|
|
7687
|
+
versions: [version(p.sample_url, p.sample_width, p.sample_height)]
|
|
7688
|
+
});
|
|
7415
7689
|
});
|
|
7416
7690
|
}
|
|
7417
7691
|
function parseMoebooru(text) {
|
|
7692
|
+
const types = tagTypes(text);
|
|
7693
|
+
return records(text).flatMap((p) => {
|
|
7694
|
+
const file = absolute(String(p.file_url ?? ""));
|
|
7695
|
+
const tags = String(p.tags ?? "");
|
|
7696
|
+
return post(p, {
|
|
7697
|
+
preview: p.preview_url,
|
|
7698
|
+
file,
|
|
7699
|
+
ext: String(p.file_ext || extension(file)).toLowerCase(),
|
|
7700
|
+
owner: p.author,
|
|
7701
|
+
artist: tags.split(/\s+/).find((tag) => types.get(tag) === "artist") ?? "",
|
|
7702
|
+
md5: p.md5,
|
|
7703
|
+
tags,
|
|
7704
|
+
versions: [
|
|
7705
|
+
version(p.jpeg_url, p.jpeg_width, p.jpeg_height),
|
|
7706
|
+
version(p.sample_url, p.sample_width, p.sample_height)
|
|
7707
|
+
]
|
|
7708
|
+
});
|
|
7709
|
+
});
|
|
7710
|
+
}
|
|
7711
|
+
function parseDanbooru(text) {
|
|
7418
7712
|
return records(text).flatMap((p) => {
|
|
7419
7713
|
const file = absolute(String(p.file_url ?? ""));
|
|
7420
|
-
|
|
7714
|
+
const asset = p.media_asset ?? {};
|
|
7715
|
+
return post(p, {
|
|
7716
|
+
preview: p.preview_file_url,
|
|
7717
|
+
file,
|
|
7718
|
+
ext: String(p.file_ext || extension(file)).toLowerCase(),
|
|
7719
|
+
owner: "",
|
|
7720
|
+
artist: String(p.tag_string_artist ?? "").split(/\s+/)[0] ?? "",
|
|
7721
|
+
md5: p.md5,
|
|
7722
|
+
tags: String(p.tag_string ?? ""),
|
|
7723
|
+
versions: (asset.variants ?? []).map((v) => version(v.url, v.width, v.height)).filter((v) => v.ext === "jpg" || v.ext === "png").sort((a, b) => b.width * b.height - a.width * a.height)
|
|
7724
|
+
});
|
|
7421
7725
|
});
|
|
7422
7726
|
}
|
|
7423
7727
|
function parseCount(xml) {
|
|
7424
7728
|
return Number(/count="(\d+)"/.exec(xml)?.[1] ?? 0);
|
|
7425
7729
|
}
|
|
7426
|
-
function
|
|
7427
|
-
const
|
|
7730
|
+
function parseCounts(text) {
|
|
7731
|
+
const raw = text.trim() ? JSON.parse(text) : {};
|
|
7732
|
+
return Number(raw.counts?.posts) || 0;
|
|
7733
|
+
}
|
|
7734
|
+
function cutoutMap(spec) {
|
|
7735
|
+
const cutouts = new Map;
|
|
7736
|
+
for (const entry of (spec ?? "").split(/\s+/).filter(Boolean)) {
|
|
7737
|
+
const at2 = entry.indexOf("=");
|
|
7738
|
+
if (at2 > 0) {
|
|
7739
|
+
cutouts.set(entry.slice(0, at2), entry.slice(at2 + 1).split(",").filter(Boolean));
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7742
|
+
return cutouts;
|
|
7743
|
+
}
|
|
7744
|
+
function hostMap(spec) {
|
|
7745
|
+
const hosts = new Map;
|
|
7746
|
+
for (const entry of (spec ?? "").split(/[\s,]+/).filter(Boolean)) {
|
|
7747
|
+
const at2 = entry.indexOf("=");
|
|
7748
|
+
const key = entry.slice(0, at2);
|
|
7749
|
+
try {
|
|
7750
|
+
const url = new URL(entry.slice(at2 + 1));
|
|
7751
|
+
if (key && url.protocol === "https:") {
|
|
7752
|
+
hosts.set(key, url.origin);
|
|
7753
|
+
}
|
|
7754
|
+
} catch {}
|
|
7755
|
+
}
|
|
7756
|
+
return hosts;
|
|
7757
|
+
}
|
|
7758
|
+
function chosen(value, all, fallback, none) {
|
|
7759
|
+
const words = (value ?? "").split(/\s+/);
|
|
7760
|
+
if (none !== undefined && words.includes(none)) {
|
|
7761
|
+
return [];
|
|
7762
|
+
}
|
|
7763
|
+
const picked = all.filter((item) => words.includes(item));
|
|
7764
|
+
return picked.length > 0 ? picked : [...fallback];
|
|
7765
|
+
}
|
|
7766
|
+
function ratingSet(value) {
|
|
7767
|
+
return chosen(value, RATINGS, ["safe"]);
|
|
7768
|
+
}
|
|
7769
|
+
function blockSet(value) {
|
|
7770
|
+
return chosen(value, BLOCKS, BLOCKS, "none");
|
|
7771
|
+
}
|
|
7772
|
+
function tiers(safe, questionable, explicit) {
|
|
7773
|
+
return { safe: new Set(safe), questionable: new Set(questionable), explicit: new Set(explicit) };
|
|
7774
|
+
}
|
|
7775
|
+
function based(spec) {
|
|
7776
|
+
const origin = HOSTS.get(spec.key);
|
|
7777
|
+
const cutouts = CUTOUTS.get(spec.key);
|
|
7428
7778
|
return {
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
origin,
|
|
7432
|
-
cutouts,
|
|
7433
|
-
vouched
|
|
7434
|
-
|
|
7779
|
+
...spec,
|
|
7780
|
+
origin: origin ?? spec.origin,
|
|
7781
|
+
moved: origin !== undefined && origin !== spec.origin,
|
|
7782
|
+
cutouts: cutouts ?? spec.cutouts,
|
|
7783
|
+
vouched: spec.vouched && cutouts === undefined
|
|
7784
|
+
};
|
|
7785
|
+
}
|
|
7786
|
+
function anyOf(tags, grouped) {
|
|
7787
|
+
if (tags.length < 2) {
|
|
7788
|
+
return tags.join("");
|
|
7789
|
+
}
|
|
7790
|
+
return grouped ? `( ${tags.join(" ~ ")} )` : tags.map((tag) => `~${tag}`).join(" ");
|
|
7791
|
+
}
|
|
7792
|
+
function gelbooru(raw) {
|
|
7793
|
+
const spec = based(raw);
|
|
7794
|
+
const api = (params) => `${spec.origin}/index.php?${new URLSearchParams({ page: "dapi", s: "post", q: "index", ...params })}`;
|
|
7795
|
+
return {
|
|
7796
|
+
...spec,
|
|
7797
|
+
cutouts: anyOf(spec.cutouts, true),
|
|
7798
|
+
ratings: tiers(["safe", "general"], ["questionable"], ["explicit"]),
|
|
7799
|
+
rate: () => "",
|
|
7800
|
+
best: "sort:score:desc",
|
|
7801
|
+
tagBudget: Number.POSITIVE_INFINITY,
|
|
7435
7802
|
postsUrl: (tags, page) => api({ json: "1", limit: String(PAGE), pid: String(page), tags }),
|
|
7436
7803
|
countUrl: (tags) => api({ limit: "0", tags }),
|
|
7437
7804
|
postUrl: (id) => api({ json: "1", id: String(id) }),
|
|
7438
|
-
pageUrl: (id) => `${origin}/index.php?page=post&s=view&id=${id}`,
|
|
7439
|
-
parse: (text) => parseGelbooru(text, origin)
|
|
7805
|
+
pageUrl: (id) => `${spec.origin}/index.php?page=post&s=view&id=${id}`,
|
|
7806
|
+
parse: (text) => parseGelbooru(text, spec.origin),
|
|
7807
|
+
count: parseCount
|
|
7440
7808
|
};
|
|
7441
7809
|
}
|
|
7442
|
-
function moebooru(
|
|
7443
|
-
const
|
|
7810
|
+
function moebooru(raw) {
|
|
7811
|
+
const spec = based(raw);
|
|
7812
|
+
const params = (rest) => new URLSearchParams({ api_version: "2", include_tags: "1", ...rest });
|
|
7444
7813
|
return {
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7814
|
+
...spec,
|
|
7815
|
+
cutouts: anyOf(spec.cutouts, false),
|
|
7816
|
+
ratings: tiers([MOEBOORU.safe], [MOEBOORU.questionable], [MOEBOORU.explicit]),
|
|
7817
|
+
rate: (levels) => {
|
|
7818
|
+
const [only] = levels;
|
|
7819
|
+
if (levels.length === 1 && only) {
|
|
7820
|
+
return `rating:${MOEBOORU[only]}`;
|
|
7821
|
+
}
|
|
7822
|
+
const missing = RATINGS.filter((level) => !levels.includes(level));
|
|
7823
|
+
const [left] = missing;
|
|
7824
|
+
return missing.length === 1 && left ? `-rating:${MOEBOORU[left]}` : "";
|
|
7825
|
+
},
|
|
7826
|
+
best: "order:score",
|
|
7827
|
+
tagBudget: Number.POSITIVE_INFINITY,
|
|
7828
|
+
postsUrl: (tags, page) => `${spec.origin}/post.json?${params({ limit: String(PAGE), page: String(page + 1), tags })}`,
|
|
7829
|
+
countUrl: (tags) => `${spec.origin}/post.xml?${new URLSearchParams({ limit: "1", tags })}`,
|
|
7830
|
+
postUrl: (id) => `${spec.origin}/post.json?${params({ tags: `id:${id}` })}`,
|
|
7831
|
+
pageUrl: (id) => `${spec.origin}/post/show/${id}`,
|
|
7832
|
+
parse: parseMoebooru,
|
|
7833
|
+
count: parseCount
|
|
7834
|
+
};
|
|
7835
|
+
}
|
|
7836
|
+
function danbooru(raw) {
|
|
7837
|
+
const spec = based(raw);
|
|
7838
|
+
return {
|
|
7839
|
+
...spec,
|
|
7840
|
+
cutouts: anyOf(spec.cutouts, false),
|
|
7841
|
+
ratings: tiers(["g"], ["s", "q"], ["e"]),
|
|
7842
|
+
rate: () => "",
|
|
7843
|
+
best: "order:score",
|
|
7844
|
+
tagBudget: 2,
|
|
7845
|
+
postsUrl: (tags, page) => `${spec.origin}/posts.json?${new URLSearchParams({ limit: String(PAGE), page: String(page + 1), tags })}`,
|
|
7846
|
+
countUrl: (tags) => `${spec.origin}/counts/posts.json?${new URLSearchParams({ tags })}`,
|
|
7847
|
+
postUrl: (id) => `${spec.origin}/posts.json?${new URLSearchParams({ limit: "1", tags: `id:${id}` })}`,
|
|
7848
|
+
pageUrl: (id) => `${spec.origin}/posts/${id}`,
|
|
7849
|
+
parse: parseDanbooru,
|
|
7850
|
+
count: parseCounts
|
|
7456
7851
|
};
|
|
7457
7852
|
}
|
|
7853
|
+
function siteNamed(name) {
|
|
7854
|
+
return SITES.find((site) => site.key === name || site.name === name || new URL(site.origin).host === name);
|
|
7855
|
+
}
|
|
7856
|
+
function tagsOf(query) {
|
|
7857
|
+
return query.split(/\s+/).filter(Boolean).length;
|
|
7858
|
+
}
|
|
7859
|
+
function postRef(text, fallback) {
|
|
7860
|
+
const query = text.trim();
|
|
7861
|
+
if (/^\d+$/.test(query)) {
|
|
7862
|
+
return { site: fallback, id: Number(query) };
|
|
7863
|
+
}
|
|
7864
|
+
const named = /^([\w.]+):(\d+)$/.exec(query);
|
|
7865
|
+
if (named) {
|
|
7866
|
+
const site2 = SITES.find((s) => s.key === named[1] || s.name === named[1]);
|
|
7867
|
+
return site2 && { site: site2, id: Number(named[2]) };
|
|
7868
|
+
}
|
|
7869
|
+
let url;
|
|
7870
|
+
try {
|
|
7871
|
+
url = new URL(query);
|
|
7872
|
+
} catch {
|
|
7873
|
+
return;
|
|
7874
|
+
}
|
|
7875
|
+
const site = siteNamed(url.host);
|
|
7876
|
+
const id = Number(url.searchParams.get("id") ?? url.pathname.split("/").filter(Boolean).at(-1));
|
|
7877
|
+
return site && id > 0 ? { site, id } : undefined;
|
|
7878
|
+
}
|
|
7458
7879
|
var SITES = [
|
|
7459
|
-
gelbooru(
|
|
7460
|
-
|
|
7461
|
-
|
|
7880
|
+
gelbooru({
|
|
7881
|
+
key: "safebooru",
|
|
7882
|
+
name: "safebooru",
|
|
7883
|
+
origin: "https://safebooru.org",
|
|
7884
|
+
cutouts: ["transparent_background", "vector_trace"],
|
|
7885
|
+
vouched: false,
|
|
7886
|
+
ansi: 4
|
|
7887
|
+
}),
|
|
7888
|
+
moebooru({
|
|
7889
|
+
key: "yande",
|
|
7890
|
+
name: "yande.re",
|
|
7891
|
+
origin: "https://yande.re",
|
|
7892
|
+
cutouts: ["transparent_png"],
|
|
7893
|
+
vouched: true,
|
|
7894
|
+
ansi: 5
|
|
7895
|
+
}),
|
|
7896
|
+
moebooru({
|
|
7897
|
+
key: "konachan",
|
|
7898
|
+
name: "konachan",
|
|
7899
|
+
origin: "https://konachan.net",
|
|
7900
|
+
cutouts: ["transparent", "vector"],
|
|
7901
|
+
vouched: false,
|
|
7902
|
+
ansi: 6
|
|
7903
|
+
}),
|
|
7904
|
+
danbooru({
|
|
7905
|
+
key: "danbooru",
|
|
7906
|
+
name: "danbooru",
|
|
7907
|
+
origin: "https://safebooru.donmai.us",
|
|
7908
|
+
cutouts: ["transparent_background"],
|
|
7909
|
+
vouched: false,
|
|
7910
|
+
ansi: 2
|
|
7911
|
+
})
|
|
7462
7912
|
];
|
|
7463
7913
|
function originHost(source) {
|
|
7464
7914
|
let host;
|
|
@@ -7469,8 +7919,17 @@ function originHost(source) {
|
|
|
7469
7919
|
}
|
|
7470
7920
|
return host.split(".").slice(-2).join(".");
|
|
7471
7921
|
}
|
|
7472
|
-
function
|
|
7473
|
-
return
|
|
7922
|
+
function exposed(post2, blocks = BLOCKS) {
|
|
7923
|
+
return post2.tags.filter((tag) => blocks.some((block2) => EXPOSED[block2].has(tag)));
|
|
7924
|
+
}
|
|
7925
|
+
function rated(site, post2, levels = ["safe"]) {
|
|
7926
|
+
return levels.some((level) => site.ratings[level].has(post2.rating));
|
|
7927
|
+
}
|
|
7928
|
+
function rendition(post2) {
|
|
7929
|
+
return [post2, ...post2.smaller].find((v) => v.width * v.height <= MAX_PIXELS);
|
|
7930
|
+
}
|
|
7931
|
+
function mirrored(owner) {
|
|
7932
|
+
return MIRRORS.has(owner);
|
|
7474
7933
|
}
|
|
7475
7934
|
function mates(owners) {
|
|
7476
7935
|
const byOwner = new Map;
|
|
@@ -7482,24 +7941,83 @@ function mates(owners) {
|
|
|
7482
7941
|
}
|
|
7483
7942
|
return byOwner;
|
|
7484
7943
|
}
|
|
7944
|
+
function cacheRoot() {
|
|
7945
|
+
return join4(process.env.XDG_CACHE_HOME ?? join4(homedir(), ".cache"), "ttheme");
|
|
7946
|
+
}
|
|
7485
7947
|
function cacheDir(site) {
|
|
7486
|
-
return join4(
|
|
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
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
function retryAfter(value, now) {
|
|
7978
|
+
if (value !== null && /^\s*\d+\s*$/.test(value)) {
|
|
7979
|
+
return Number(value) * 1000;
|
|
7980
|
+
}
|
|
7981
|
+
const at2 = value === null ? Number.NaN : Date.parse(value);
|
|
7982
|
+
return Number.isNaN(at2) ? MAX_WAIT : Math.max(0, at2 - now);
|
|
7983
|
+
}
|
|
7984
|
+
function pausedUntil(site) {
|
|
7985
|
+
return paused.get(site.key) ?? 0;
|
|
7986
|
+
}
|
|
7987
|
+
function challenged(response) {
|
|
7988
|
+
return (response.headers.get("server") ?? "").startsWith("cloudflare") && response.headers.get("cf-mitigated") === "challenge";
|
|
7487
7989
|
}
|
|
7488
7990
|
async function get(site, url, signal, headers = {}, timeout = TIMEOUT) {
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7991
|
+
for (let tries = 1;; tries++) {
|
|
7992
|
+
const wait = pausedUntil(site) - Date.now();
|
|
7993
|
+
if (wait > 0) {
|
|
7994
|
+
await sleep(wait, undefined, { signal });
|
|
7995
|
+
}
|
|
7996
|
+
const response = await fetch(url, {
|
|
7997
|
+
headers: { "User-Agent": AGENT, Referer: `${site.origin}/`, ...headers },
|
|
7998
|
+
signal: timeout ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
7999
|
+
});
|
|
8000
|
+
if (response.status === 429 && tries < TRIES) {
|
|
8001
|
+
await response.body?.cancel();
|
|
8002
|
+
const delay = retryAfter(response.headers.get("retry-after"), Date.now());
|
|
8003
|
+
if (delay > MAX_WAIT) {
|
|
8004
|
+
throw new Error(`${site.name} asks to wait ${Math.ceil(delay / 1000)}s`);
|
|
8005
|
+
}
|
|
8006
|
+
paused.set(site.key, Math.max(pausedUntil(site), Date.now() + delay));
|
|
8007
|
+
continue;
|
|
8008
|
+
}
|
|
8009
|
+
if (!response.ok) {
|
|
8010
|
+
await response.body?.cancel();
|
|
8011
|
+
throw new Error(challenged(response) ? `${site.name} is behind a Cloudflare challenge` : `${site.name} answered ${response.status}`);
|
|
8012
|
+
}
|
|
8013
|
+
return response;
|
|
7495
8014
|
}
|
|
7496
|
-
return response;
|
|
7497
8015
|
}
|
|
7498
8016
|
async function fetchPosts(site, tags, page, signal) {
|
|
7499
8017
|
return site.parse(await (await get(site, site.postsUrl(tags, page), signal)).text());
|
|
7500
8018
|
}
|
|
7501
8019
|
async function fetchCount(site, tags, signal) {
|
|
7502
|
-
return
|
|
8020
|
+
return site.count(await (await get(site, site.countUrl(tags), signal)).text());
|
|
7503
8021
|
}
|
|
7504
8022
|
async function fetchPost(site, id, signal) {
|
|
7505
8023
|
const [found] = site.parse(await (await get(site, site.postUrl(id), signal)).text());
|
|
@@ -7537,7 +8055,7 @@ async function fetchBytes(site, url, signal, progress) {
|
|
|
7537
8055
|
}
|
|
7538
8056
|
|
|
7539
8057
|
// src/catalog.ts
|
|
7540
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as
|
|
8058
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7541
8059
|
import { dirname, join as join5 } from "node:path";
|
|
7542
8060
|
var REGISTRY_URL = "https://kecan0406.github.io/ttheme/manifest.json";
|
|
7543
8061
|
function catalogPath(configHome) {
|
|
@@ -7554,7 +8072,7 @@ function readCatalog(configHome) {
|
|
|
7554
8072
|
if (!existsSync(path2)) {
|
|
7555
8073
|
throw new Error(`no catalog at ${path2} — run \`ttheme init\` first`);
|
|
7556
8074
|
}
|
|
7557
|
-
return parseCatalog(
|
|
8075
|
+
return parseCatalog(readFileSync3(path2, "utf8"));
|
|
7558
8076
|
}
|
|
7559
8077
|
function parseCatalog(source) {
|
|
7560
8078
|
let doc;
|
|
@@ -7614,8 +8132,61 @@ function search(palettes, query) {
|
|
|
7614
8132
|
return palettes.filter((p) => [p.name, p.group, p.native ?? "", p.ansiSource].some((field) => field.toLowerCase().includes(needle)));
|
|
7615
8133
|
}
|
|
7616
8134
|
|
|
8135
|
+
// src/cutout.ts
|
|
8136
|
+
import { spawn } from "node:child_process";
|
|
8137
|
+
var TIMEOUT2 = 30000;
|
|
8138
|
+
var LEAST = 3;
|
|
8139
|
+
var MOST = 97;
|
|
8140
|
+
var SCRIPT = `ObjC.import('Vision')
|
|
8141
|
+
ObjC.import('CoreImage')
|
|
8142
|
+
ObjC.import('CoreGraphics')
|
|
8143
|
+
function run(argv) {
|
|
8144
|
+
const error = Ref()
|
|
8145
|
+
const image = $.CIImage.imageWithContentsOfURL($.NSURL.fileURLWithPath(argv[0]))
|
|
8146
|
+
if (!image) throw new Error('unreadable image')
|
|
8147
|
+
const handler = $.VNImageRequestHandler.alloc.initWithCIImageOptions(image, $({}))
|
|
8148
|
+
const request = $.VNGenerateForegroundInstanceMaskRequest.alloc.init
|
|
8149
|
+
if (!handler.performRequestsError($([request]), error)) throw new Error('vision failed')
|
|
8150
|
+
if (request.results.count === 0) throw new Error('no foreground')
|
|
8151
|
+
const found = request.results.objectAtIndex(0)
|
|
8152
|
+
const buffer = found.generateMaskedImageOfInstancesFromRequestHandlerCroppedToInstancesExtentError(
|
|
8153
|
+
found.allInstances, handler, false, error)
|
|
8154
|
+
if (!buffer) throw new Error('mask failed')
|
|
8155
|
+
const written = $.CIContext.context.writePNGRepresentationOfImageToURLFormatColorSpaceOptionsError(
|
|
8156
|
+
$.CIImage.imageWithCVPixelBuffer(buffer), $.NSURL.fileURLWithPath(argv[1]), $.kCIFormatRGBA8,
|
|
8157
|
+
$.CGColorSpaceCreateWithName($.kCGColorSpaceSRGB), $({}), error)
|
|
8158
|
+
if (!written) throw new Error('write failed')
|
|
8159
|
+
}`;
|
|
8160
|
+
function canRemoveBackground() {
|
|
8161
|
+
return process.platform === "darwin";
|
|
8162
|
+
}
|
|
8163
|
+
function keepable(clear) {
|
|
8164
|
+
return clear >= LEAST && clear <= MOST;
|
|
8165
|
+
}
|
|
8166
|
+
function removeBackground(input, output, signal) {
|
|
8167
|
+
return new Promise((resolve, reject) => {
|
|
8168
|
+
const child = spawn("osascript", ["-l", "JavaScript", "-e", SCRIPT, input, output], {
|
|
8169
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
8170
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(TIMEOUT2)])
|
|
8171
|
+
});
|
|
8172
|
+
let failure = "";
|
|
8173
|
+
child.stderr.on("data", (chunk) => {
|
|
8174
|
+
failure += chunk;
|
|
8175
|
+
});
|
|
8176
|
+
child.on("error", reject);
|
|
8177
|
+
child.on("close", (code) => {
|
|
8178
|
+
if (code === 0) {
|
|
8179
|
+
resolve();
|
|
8180
|
+
return;
|
|
8181
|
+
}
|
|
8182
|
+
reject(new Error(failure.trim().split(`
|
|
8183
|
+
`).at(-1) || `osascript exited ${code}`));
|
|
8184
|
+
});
|
|
8185
|
+
});
|
|
8186
|
+
}
|
|
8187
|
+
|
|
7617
8188
|
// src/find-screen.ts
|
|
7618
|
-
var TILE = { pitch: 25, cols: 22, rows: 9, height:
|
|
8189
|
+
var TILE = { pitch: 25, cols: 22, rows: 9, height: 13 };
|
|
7619
8190
|
var MIN = { cols: 25, rows: 16 };
|
|
7620
8191
|
var TRY_ID = 2 ** 31;
|
|
7621
8192
|
var BELOW_BG = -1073741826;
|
|
@@ -7652,14 +8223,14 @@ function decodeKeys(input) {
|
|
|
7652
8223
|
if (m) {
|
|
7653
8224
|
keys.push(CSI[`${m[1] ?? ""}${m[2] ?? m[3] ?? ""}`] ?? "nop");
|
|
7654
8225
|
i += 1 + m[0].length;
|
|
7655
|
-
|
|
7656
|
-
keys.push("esc");
|
|
7657
|
-
i++;
|
|
8226
|
+
continue;
|
|
7658
8227
|
}
|
|
8228
|
+
keys.push("esc");
|
|
8229
|
+
i++;
|
|
7659
8230
|
continue;
|
|
7660
8231
|
}
|
|
7661
8232
|
keys.push(c === "\r" || c === `
|
|
7662
|
-
` ? "enter" : c === "\t" ? "tab" : c === "\x03" ? "ctrl-c" : c);
|
|
8233
|
+
` ? "enter" : c === "\t" ? "tab" : c === "\x03" ? "ctrl-c" : c === "" || c === "\b" ? "backspace" : c);
|
|
7663
8234
|
i++;
|
|
7664
8235
|
}
|
|
7665
8236
|
return keys;
|
|
@@ -7730,8 +8301,14 @@ function progress(got, size) {
|
|
|
7730
8301
|
}
|
|
7731
8302
|
return size >= 1e6 ? `${(got / 1e6).toFixed(1)}/${(size / 1e6).toFixed(1)} MB` : `${Math.round(got / 1000)}/${Math.round(size / 1000)} KB`;
|
|
7732
8303
|
}
|
|
8304
|
+
function plain(artist) {
|
|
8305
|
+
return artist.replace(/_\([^)]*\)?$/, "");
|
|
8306
|
+
}
|
|
7733
8307
|
function dims(tile) {
|
|
7734
|
-
return [
|
|
8308
|
+
return [
|
|
8309
|
+
`${tile.reduced ? "↓" : ""}${tile.width}×${tile.height}`,
|
|
8310
|
+
Math.max(tile.width, tile.height) < SMALL ? YELLOW : D
|
|
8311
|
+
];
|
|
7735
8312
|
}
|
|
7736
8313
|
function foot(line, cols, accent, spec) {
|
|
7737
8314
|
const keys = [...spec.keys ?? []];
|
|
@@ -7854,23 +8431,61 @@ function frameBox(lines, r0, c0, h, w, sgr) {
|
|
|
7854
8431
|
}
|
|
7855
8432
|
lines[r0 + h - 1]?.put(c0, `╰${"─".repeat(w - 2)}╯`, sgr);
|
|
7856
8433
|
}
|
|
7857
|
-
function badge(view) {
|
|
7858
|
-
return [` ${
|
|
8434
|
+
function badge(view, label = view.site) {
|
|
8435
|
+
return [` ${label} `, `\x1B[7;${30 + view.siteAnsi}m`];
|
|
8436
|
+
}
|
|
8437
|
+
function tabs(line, cols, view) {
|
|
8438
|
+
const strip = SITES.flatMap((site, i) => {
|
|
8439
|
+
const label = `${site.name}${site.moved ? "*" : ""}`;
|
|
8440
|
+
const tab = site.name === view.site ? badge(view, label) : [` ${label} `, D];
|
|
8441
|
+
return i ? [[" ", ""], tab] : [tab];
|
|
8442
|
+
});
|
|
8443
|
+
const fits = strip.reduce((n, [text]) => n + width(text), 0) <= cols;
|
|
8444
|
+
line.run(0, fits ? strip : [badge(view)]);
|
|
7859
8445
|
}
|
|
7860
8446
|
function query(line, cols, view, accent) {
|
|
8447
|
+
if (view.editing !== undefined) {
|
|
8448
|
+
const c2 = line.run(0, [
|
|
8449
|
+
["⌕ ", accent],
|
|
8450
|
+
[view.editing, ""],
|
|
8451
|
+
["█", accent]
|
|
8452
|
+
]);
|
|
8453
|
+
line.put(c2, view.editing ? " enter searches" : ` a tag, a post url or an id — ${view.tag || "esc leaves"}`, D);
|
|
8454
|
+
return;
|
|
8455
|
+
}
|
|
7861
8456
|
const c = line.run(0, [
|
|
7862
8457
|
["⌕ ", accent],
|
|
7863
|
-
[view.tag, ""]
|
|
8458
|
+
[view.tag || "nothing yet — / searches", view.tag ? "" : D]
|
|
7864
8459
|
]);
|
|
7865
|
-
|
|
8460
|
+
const allowed = BLOCKS.filter((block2) => !view.block.includes(block2));
|
|
8461
|
+
const state = [
|
|
8462
|
+
view.preset,
|
|
8463
|
+
view.order,
|
|
8464
|
+
...view.rating.join("+") === "safe" ? [] : [view.rating.join("+")],
|
|
8465
|
+
...allowed.length > 0 ? [`allows ${allowed.join("+")}`] : [],
|
|
8466
|
+
...view.unblocked ? ["unblock"] : []
|
|
8467
|
+
];
|
|
8468
|
+
line.put(c, ` ${state.join(" ")}`, D);
|
|
7866
8469
|
if (view.total > 0 || !view.searching) {
|
|
7867
|
-
const counter = [
|
|
8470
|
+
const counter = [[`${view.tiles.length}/${view.checked}`, ""]];
|
|
7868
8471
|
if (view.checked < view.total) {
|
|
7869
8472
|
counter.push([` of ${view.total}`, D]);
|
|
7870
8473
|
}
|
|
7871
8474
|
line.right(cols, counter);
|
|
7872
8475
|
}
|
|
7873
8476
|
}
|
|
8477
|
+
function transparent(shown) {
|
|
8478
|
+
if (shown.cut === "on") {
|
|
8479
|
+
return [[`cut out · ${shown.clear}% transparent`, GREEN]];
|
|
8480
|
+
}
|
|
8481
|
+
if (shown.cut === "off") {
|
|
8482
|
+
return [["opaque · x cuts out", YELLOW]];
|
|
8483
|
+
}
|
|
8484
|
+
if (shown.cut === "failed") {
|
|
8485
|
+
return [["opaque · no cut-out", YELLOW]];
|
|
8486
|
+
}
|
|
8487
|
+
return [shown.clear > 0 ? [`${shown.clear}% transparent`, GREEN] : ["opaque", YELLOW]];
|
|
8488
|
+
}
|
|
7874
8489
|
function status(view) {
|
|
7875
8490
|
if (view.installing !== undefined) {
|
|
7876
8491
|
return [`installing ${view.palette} ← ${view.site} ${view.installing}`, YELLOW];
|
|
@@ -7878,18 +8493,27 @@ function status(view) {
|
|
|
7878
8493
|
if (view.error) {
|
|
7879
8494
|
return [view.error, YELLOW];
|
|
7880
8495
|
}
|
|
8496
|
+
if (view.waiting !== undefined) {
|
|
8497
|
+
return [`${view.site} asked to slow down · ${view.waiting}s`, YELLOW];
|
|
8498
|
+
}
|
|
7881
8499
|
if (view.fetching) {
|
|
7882
8500
|
return [`fetching ${view.fetching.id} · ${progress(view.fetching.got, view.fetching.size)}`, YELLOW];
|
|
7883
8501
|
}
|
|
8502
|
+
if (view.cutting !== undefined) {
|
|
8503
|
+
return [`cutting out ${view.cutting}`, YELLOW];
|
|
8504
|
+
}
|
|
7884
8505
|
if (view.preparing !== undefined) {
|
|
7885
8506
|
return [`preparing ${view.preparing}`, YELLOW];
|
|
7886
8507
|
}
|
|
8508
|
+
if (view.note) {
|
|
8509
|
+
return [view.note, D];
|
|
8510
|
+
}
|
|
7887
8511
|
return;
|
|
7888
8512
|
}
|
|
7889
8513
|
function grid(lines, images, cols, rows, view, accent) {
|
|
7890
8514
|
const { perRow, rowsVis } = gridShape(cols, rows);
|
|
7891
8515
|
query(lines[0], cols, view, accent);
|
|
7892
|
-
lines[1]
|
|
8516
|
+
tabs(lines[1], cols, view);
|
|
7893
8517
|
for (let k = 0;k < perRow * rowsVis; k++) {
|
|
7894
8518
|
const i = view.top * perRow + k;
|
|
7895
8519
|
const tile = view.tiles[i];
|
|
@@ -7909,6 +8533,20 @@ function grid(lines, images, cols, rows, view, accent) {
|
|
|
7909
8533
|
if (tile.mates.length > 0) {
|
|
7910
8534
|
lines[r0 + 10]?.put(c0 + TILE.cols, "≈", accent);
|
|
7911
8535
|
}
|
|
8536
|
+
const credit = tile.artist ? plain(tile.artist) : tile.owner && `@${tile.owner}`;
|
|
8537
|
+
if (credit) {
|
|
8538
|
+
lines[r0 + 11]?.put(c0 + 1, credit.slice(0, TILE.cols - 8), D);
|
|
8539
|
+
}
|
|
8540
|
+
const marks = [];
|
|
8541
|
+
if (tile.score > 0) {
|
|
8542
|
+
marks.push([`★${tile.score}`, D]);
|
|
8543
|
+
}
|
|
8544
|
+
if (tile.variants > 1) {
|
|
8545
|
+
marks.push([` ×${tile.variants}`, accent]);
|
|
8546
|
+
}
|
|
8547
|
+
if (marks.length > 0) {
|
|
8548
|
+
lines[r0 + 11]?.right(c0 + TILE.cols + 1, marks);
|
|
8549
|
+
}
|
|
7912
8550
|
}
|
|
7913
8551
|
const above = view.top * perRow;
|
|
7914
8552
|
const below = view.tiles.length - (view.top + rowsVis) * perRow;
|
|
@@ -7917,7 +8555,7 @@ function grid(lines, images, cols, rows, view, accent) {
|
|
|
7917
8555
|
lines[rows - 2]?.put(0, scroll.join(" "), D);
|
|
7918
8556
|
}
|
|
7919
8557
|
if (!view.searching && view.tiles.length === 0 && !view.error) {
|
|
7920
|
-
lines[3]?.put(0, view.preset === "cutouts" ? `no transparent cutouts of ${view.tag} on ${view.site} —
|
|
8558
|
+
lines[3]?.put(0, view.preset === "cutouts" ? `no transparent cutouts of ${view.tag} on ${view.site} — c searches every post, tab tries ${view.nextSite}` : `no posts of ${view.tag} on ${view.site} — tab tries ${view.nextSite}`, D);
|
|
7921
8559
|
}
|
|
7922
8560
|
const line = lines[rows - 1];
|
|
7923
8561
|
const lead = status(view);
|
|
@@ -7935,8 +8573,9 @@ function grid(lines, images, cols, rows, view, accent) {
|
|
|
7935
8573
|
keys: [
|
|
7936
8574
|
["←↑↓→", "move"],
|
|
7937
8575
|
["enter", "try on"],
|
|
7938
|
-
["tab",
|
|
7939
|
-
["
|
|
8576
|
+
["tab", "site"],
|
|
8577
|
+
["s", "settings"],
|
|
8578
|
+
["/", "search"],
|
|
7940
8579
|
["?", "keys"]
|
|
7941
8580
|
],
|
|
7942
8581
|
right: ["esc", "back"]
|
|
@@ -7949,6 +8588,12 @@ function trial(lines, images, cols, rows, view, accent) {
|
|
|
7949
8588
|
}
|
|
7950
8589
|
const shown = view.shown?.id === tile.id ? view.shown : undefined;
|
|
7951
8590
|
const meta2 = [badge(view), [" ", ""], [String(tile.id), B], [" ", ""], dims(tile)];
|
|
8591
|
+
if (tile.artist) {
|
|
8592
|
+
meta2.push([` · ${plain(tile.artist)}`, ""]);
|
|
8593
|
+
}
|
|
8594
|
+
if (tile.score > 0) {
|
|
8595
|
+
meta2.push([` · ★${tile.score}`, D]);
|
|
8596
|
+
}
|
|
7952
8597
|
if (shown) {
|
|
7953
8598
|
meta2.push([` · ${megabytes(shown.bytes)}`, D]);
|
|
7954
8599
|
}
|
|
@@ -7957,7 +8602,7 @@ function trial(lines, images, cols, rows, view, accent) {
|
|
|
7957
8602
|
meta2.push([` · ${tile.origin}`, D]);
|
|
7958
8603
|
}
|
|
7959
8604
|
if (shown) {
|
|
7960
|
-
meta2.push([" ", ""], shown
|
|
8605
|
+
meta2.push([" ", ""], ...transparent(shown));
|
|
7961
8606
|
}
|
|
7962
8607
|
lines[0]?.run(0, meta2);
|
|
7963
8608
|
if (tile.mates.length > 0) {
|
|
@@ -7977,6 +8622,7 @@ function trial(lines, images, cols, rows, view, accent) {
|
|
|
7977
8622
|
keys: view.installing === undefined ? [
|
|
7978
8623
|
["←→", "browse"],
|
|
7979
8624
|
["enter", "install"],
|
|
8625
|
+
...view.shown?.cut === "on" || view.shown?.cut === "off" ? [["x", "cut out"]] : [],
|
|
7980
8626
|
["?", "keys"]
|
|
7981
8627
|
] : [],
|
|
7982
8628
|
right: view.installing === undefined ? ["esc", "grid"] : undefined
|
|
@@ -7986,18 +8632,60 @@ var KEYS = {
|
|
|
7986
8632
|
grid: [
|
|
7987
8633
|
["move", "←↑↓→ home end pgup pgdn"],
|
|
7988
8634
|
["try on", "enter"],
|
|
7989
|
-
["
|
|
7990
|
-
["
|
|
8635
|
+
["site", `tab ${SITES.map((site) => site.name).join(", ")}`],
|
|
8636
|
+
["posts", "c cutouts or every post"],
|
|
8637
|
+
["search", "/ a tag, a post url or an id"],
|
|
8638
|
+
["unfold", "space a set of ×N"],
|
|
8639
|
+
["open", "o the post page in a browser"],
|
|
8640
|
+
["settings", "s rating, block, posts, order, sets, remove bg"],
|
|
7991
8641
|
["back", "esc returns to preview"],
|
|
7992
8642
|
["close", "? esc"]
|
|
7993
8643
|
],
|
|
7994
8644
|
try: [
|
|
7995
8645
|
["browse", "←→"],
|
|
7996
8646
|
["install", "enter"],
|
|
8647
|
+
["cut out", "x the background off or on, on an opaque picture"],
|
|
8648
|
+
["open", "o the post page in a browser"],
|
|
7997
8649
|
["grid", "esc"],
|
|
7998
8650
|
["close", "? esc"]
|
|
7999
8651
|
]
|
|
8000
8652
|
};
|
|
8653
|
+
function panel(lines, cols, rows, view, accent) {
|
|
8654
|
+
const at2 = view.panel ?? 0;
|
|
8655
|
+
const label = Math.max(...view.settings.map((row) => row.label.length));
|
|
8656
|
+
const widest = Math.max(...view.settings.map((row) => row.choices.reduce((n, c) => n + c.length + (row.multi ? 6 : 3), 0)));
|
|
8657
|
+
const w = Math.min(cols - 2, Math.max(28, label + widest + 8));
|
|
8658
|
+
const h = view.settings.length + 4;
|
|
8659
|
+
const x = Math.floor((cols - w) / 2);
|
|
8660
|
+
const y = Math.max(2, Math.floor((rows - h) / 2));
|
|
8661
|
+
lines[y]?.put(x, `╭─ settings ${"─".repeat(Math.max(0, w - 13))}╮`);
|
|
8662
|
+
for (let r = y + 1;r < y + h - 1; r++) {
|
|
8663
|
+
lines[r]?.put(x, `│${" ".repeat(w - 2)}│`);
|
|
8664
|
+
}
|
|
8665
|
+
view.settings.forEach((row, i) => {
|
|
8666
|
+
const line = lines[y + 2 + i];
|
|
8667
|
+
line.put(x + 3, row.label, i === at2 ? B : D);
|
|
8668
|
+
let c = x + 5 + label;
|
|
8669
|
+
const on = row.value.split(" ");
|
|
8670
|
+
row.choices.forEach((choice, k) => {
|
|
8671
|
+
const lit = on.includes(choice);
|
|
8672
|
+
const under = row.multi && i === at2 && k === row.cursor ? "4;" : "";
|
|
8673
|
+
const text = row.multi ? ` ${lit ? "[x]" : "[ ]"} ${choice} ` : ` ${choice} `;
|
|
8674
|
+
c = line.put(c, text, lit ? `\x1B[${under}7;${30 + view.siteAnsi}m` : under ? `\x1B[4m${D}` : D) + 1;
|
|
8675
|
+
});
|
|
8676
|
+
});
|
|
8677
|
+
lines[y + h - 1]?.put(x, `╰${"─".repeat(w - 2)}╯`);
|
|
8678
|
+
foot(lines[rows - 1], cols, accent, {
|
|
8679
|
+
badge: "SET",
|
|
8680
|
+
keys: [
|
|
8681
|
+
["↑↓", "setting"],
|
|
8682
|
+
["←→", "value"],
|
|
8683
|
+
["space", "toggle"],
|
|
8684
|
+
["enter", "save"]
|
|
8685
|
+
],
|
|
8686
|
+
right: ["esc", "undo"]
|
|
8687
|
+
});
|
|
8688
|
+
}
|
|
8001
8689
|
function help(lines, cols, rows, view, accent) {
|
|
8002
8690
|
const keys = KEYS[view.mode];
|
|
8003
8691
|
const w = Math.min(cols - 2, 50);
|
|
@@ -8030,19 +8718,23 @@ function renderFind(view, cols, rows) {
|
|
|
8030
8718
|
} else {
|
|
8031
8719
|
grid(lines, images, cols, rows, view, accent);
|
|
8032
8720
|
}
|
|
8033
|
-
if (view.help) {
|
|
8721
|
+
if (view.help || view.panel !== undefined) {
|
|
8034
8722
|
const keep = view.mode === "try" ? images : [];
|
|
8035
8723
|
for (let r = 0;r < rows; r++) {
|
|
8036
8724
|
lines[r] = new Line(cols);
|
|
8037
8725
|
}
|
|
8038
|
-
|
|
8726
|
+
if (view.help) {
|
|
8727
|
+
help(lines, cols, rows, view, accent);
|
|
8728
|
+
} else {
|
|
8729
|
+
panel(lines, cols, rows, view, accent);
|
|
8730
|
+
}
|
|
8039
8731
|
return { lines: lines.map((l) => l.render()), images: keep };
|
|
8040
8732
|
}
|
|
8041
8733
|
return { lines: lines.map((l) => l.render()), images };
|
|
8042
8734
|
}
|
|
8043
8735
|
|
|
8044
8736
|
// src/palettes.ts
|
|
8045
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as
|
|
8737
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
8046
8738
|
import { homedir as homedir2 } from "node:os";
|
|
8047
8739
|
import { dirname as dirname2, join as join6 } from "node:path";
|
|
8048
8740
|
var EMITTERS = { ghostty, kitty, alacritty };
|
|
@@ -8057,7 +8749,7 @@ function readInstalled(configHome2) {
|
|
|
8057
8749
|
if (!existsSync2(path2)) {
|
|
8058
8750
|
throw new Error(`nothing installed yet at ${path2} — run \`ttheme init\` first`);
|
|
8059
8751
|
}
|
|
8060
|
-
const doc = JSON.parse(
|
|
8752
|
+
const doc = JSON.parse(readFileSync4(path2, "utf8"));
|
|
8061
8753
|
if (!Array.isArray(doc?.palettes) || !Array.isArray(doc.terminals)) {
|
|
8062
8754
|
throw new Error(`${path2} has no palettes or terminals`);
|
|
8063
8755
|
}
|
|
@@ -8152,7 +8844,7 @@ function sync(configHome2, catalog, state) {
|
|
|
8152
8844
|
}
|
|
8153
8845
|
}
|
|
8154
8846
|
const { file, body } = blockFor(terminal, configHome2, startup);
|
|
8155
|
-
const current = existsSync2(file) ?
|
|
8847
|
+
const current = existsSync2(file) ? readFileSync4(file, "utf8") : "";
|
|
8156
8848
|
if (terminal === "alacritty" && current !== "" && !current.includes("# ttheme begin")) {
|
|
8157
8849
|
continue;
|
|
8158
8850
|
}
|
|
@@ -8170,7 +8862,7 @@ function forget(configHome2, catalog, terminals, names) {
|
|
|
8170
8862
|
for (const { file } of themeFiles(terminal, toTheme(entry, catalog))) {
|
|
8171
8863
|
const path2 = join6(configHome2, terminal, "themes", file);
|
|
8172
8864
|
if (existsSync2(path2)) {
|
|
8173
|
-
|
|
8865
|
+
rmSync4(path2, { force: true });
|
|
8174
8866
|
removed.push(path2);
|
|
8175
8867
|
}
|
|
8176
8868
|
}
|
|
@@ -8179,32 +8871,310 @@ function forget(configHome2, catalog, terminals, names) {
|
|
|
8179
8871
|
return removed;
|
|
8180
8872
|
}
|
|
8181
8873
|
|
|
8182
|
-
// src/
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
var
|
|
8187
|
-
function
|
|
8188
|
-
|
|
8189
|
-
|
|
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;
|
|
8190
8887
|
}
|
|
8191
|
-
if (
|
|
8192
|
-
|
|
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;
|
|
8193
8892
|
}
|
|
8194
|
-
|
|
8195
|
-
|
|
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();
|
|
8196
8960
|
}
|
|
8197
|
-
const code = error.code;
|
|
8198
|
-
return typeof code === "string" ? code : error.message || error.name;
|
|
8199
8961
|
}
|
|
8200
|
-
|
|
8201
|
-
|
|
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
|
+
}
|
|
8202
8980
|
}
|
|
8203
|
-
|
|
8204
|
-
|
|
8981
|
+
if (!isMainThread && parentPort) {
|
|
8982
|
+
serveRenders(parentPort);
|
|
8205
8983
|
}
|
|
8206
8984
|
|
|
8207
|
-
|
|
8985
|
+
// src/unblock.ts
|
|
8986
|
+
import { connect, createServer } from "node:net";
|
|
8987
|
+
var SPLIT = 20;
|
|
8988
|
+
var HANDSHAKE = 22;
|
|
8989
|
+
function joined(parts) {
|
|
8990
|
+
const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0));
|
|
8991
|
+
let at2 = 0;
|
|
8992
|
+
for (const part of parts) {
|
|
8993
|
+
out.set(part, at2);
|
|
8994
|
+
at2 += part.length;
|
|
8995
|
+
}
|
|
8996
|
+
return out;
|
|
8997
|
+
}
|
|
8998
|
+
function reframe(record, at2 = SPLIT) {
|
|
8999
|
+
const body = record.subarray(5);
|
|
9000
|
+
if (record[0] !== HANDSHAKE || body.length < 2) {
|
|
9001
|
+
return [record];
|
|
9002
|
+
}
|
|
9003
|
+
const cut = Math.max(1, Math.min(at2, body.length - 1));
|
|
9004
|
+
const framed = (part) => joined([
|
|
9005
|
+
Uint8Array.from([
|
|
9006
|
+
record[0],
|
|
9007
|
+
record[1],
|
|
9008
|
+
record[2],
|
|
9009
|
+
part.length >> 8,
|
|
9010
|
+
part.length & 255
|
|
9011
|
+
]),
|
|
9012
|
+
part
|
|
9013
|
+
]);
|
|
9014
|
+
return [framed(body.subarray(0, cut)), framed(body.subarray(cut))];
|
|
9015
|
+
}
|
|
9016
|
+
function tunnelTo(client, host, port, rest) {
|
|
9017
|
+
const upstream = connect(port, host, () => {
|
|
9018
|
+
upstream.setNoDelay(true);
|
|
9019
|
+
client.write(`HTTP/1.1 200 Connection Established\r
|
|
9020
|
+
\r
|
|
9021
|
+
`);
|
|
9022
|
+
let held = rest;
|
|
9023
|
+
let framed = false;
|
|
9024
|
+
const forward = (chunk) => {
|
|
9025
|
+
if (framed) {
|
|
9026
|
+
upstream.write(chunk);
|
|
9027
|
+
return;
|
|
9028
|
+
}
|
|
9029
|
+
held = joined([held, chunk]);
|
|
9030
|
+
if (held.length < 5) {
|
|
9031
|
+
return;
|
|
9032
|
+
}
|
|
9033
|
+
const length = held[3] * 256 + held[4];
|
|
9034
|
+
if (held[0] !== HANDSHAKE) {
|
|
9035
|
+
framed = true;
|
|
9036
|
+
upstream.write(held);
|
|
9037
|
+
return;
|
|
9038
|
+
}
|
|
9039
|
+
if (held.length < 5 + length) {
|
|
9040
|
+
return;
|
|
9041
|
+
}
|
|
9042
|
+
framed = true;
|
|
9043
|
+
const [first, second] = reframe(held.subarray(0, 5 + length));
|
|
9044
|
+
upstream.write(first);
|
|
9045
|
+
setTimeout(() => {
|
|
9046
|
+
if (second) {
|
|
9047
|
+
upstream.write(second);
|
|
9048
|
+
}
|
|
9049
|
+
upstream.write(held.subarray(5 + length));
|
|
9050
|
+
}, 10);
|
|
9051
|
+
};
|
|
9052
|
+
client.on("data", forward);
|
|
9053
|
+
if (held.length > 0) {
|
|
9054
|
+
const pending = held;
|
|
9055
|
+
held = new Uint8Array(0);
|
|
9056
|
+
forward(pending);
|
|
9057
|
+
}
|
|
9058
|
+
upstream.pipe(client);
|
|
9059
|
+
});
|
|
9060
|
+
upstream.on("error", () => client.destroy());
|
|
9061
|
+
client.on("error", () => upstream.destroy());
|
|
9062
|
+
}
|
|
9063
|
+
function serve(client) {
|
|
9064
|
+
client.setNoDelay(true);
|
|
9065
|
+
let head = new Uint8Array(0);
|
|
9066
|
+
const onData = (chunk) => {
|
|
9067
|
+
head = joined([head, chunk]);
|
|
9068
|
+
const text = Buffer.from(head).toString("latin1");
|
|
9069
|
+
const end = text.indexOf(`\r
|
|
9070
|
+
\r
|
|
9071
|
+
`);
|
|
9072
|
+
if (end === -1) {
|
|
9073
|
+
return;
|
|
9074
|
+
}
|
|
9075
|
+
client.off("data", onData);
|
|
9076
|
+
const target = /^CONNECT ([^ :]+):(\d+)/.exec(text);
|
|
9077
|
+
if (!target) {
|
|
9078
|
+
client.end(`HTTP/1.1 405 Method Not Allowed\r
|
|
9079
|
+
\r
|
|
9080
|
+
`);
|
|
9081
|
+
return;
|
|
9082
|
+
}
|
|
9083
|
+
tunnelTo(client, target[1], Number(target[2]), head.subarray(end + 4));
|
|
9084
|
+
};
|
|
9085
|
+
client.on("data", onData);
|
|
9086
|
+
client.on("error", () => client.destroy());
|
|
9087
|
+
}
|
|
9088
|
+
function tunnel() {
|
|
9089
|
+
const server = createServer(serve);
|
|
9090
|
+
return new Promise((resolve2, reject) => {
|
|
9091
|
+
server.once("error", reject);
|
|
9092
|
+
server.listen(0, "127.0.0.1", () => {
|
|
9093
|
+
resolve2({
|
|
9094
|
+
port: server.address().port,
|
|
9095
|
+
close: () => {
|
|
9096
|
+
server.close();
|
|
9097
|
+
server.unref();
|
|
9098
|
+
}
|
|
9099
|
+
});
|
|
9100
|
+
});
|
|
9101
|
+
});
|
|
9102
|
+
}
|
|
9103
|
+
|
|
9104
|
+
// src/find.ts
|
|
9105
|
+
var SETTINGS = [
|
|
9106
|
+
{ name: "TTHEME_FIND_RATING", label: "rating", choices: RATINGS, multi: { read: ratingSet } },
|
|
9107
|
+
{ name: "TTHEME_FIND_BLOCK", label: "block", choices: BLOCKS, multi: { read: blockSet, none: "none" } },
|
|
9108
|
+
{ name: "TTHEME_FIND_POSTS", label: "posts", choices: ["all", "cutouts"] },
|
|
9109
|
+
{ name: "TTHEME_FIND_ORDER", label: "order", choices: ["newest", "score"] },
|
|
9110
|
+
{ name: "TTHEME_FIND_SETS", label: "sets", choices: ["fold", "show"] },
|
|
9111
|
+
...canRemoveBackground() ? [{ name: "TTHEME_FIND_REMOVE_BG", label: "remove bg", choices: ["on", "off"] }] : []
|
|
9112
|
+
];
|
|
9113
|
+
function initial(setting, raw) {
|
|
9114
|
+
if (setting.multi) {
|
|
9115
|
+
return setting.multi.read(raw).join(" ") || setting.multi.none;
|
|
9116
|
+
}
|
|
9117
|
+
return setting.choices.find((choice) => choice === raw) ?? setting.choices[0];
|
|
9118
|
+
}
|
|
9119
|
+
var PROBE = 12;
|
|
9120
|
+
var THUMB = 12;
|
|
9121
|
+
var PRELOAD = 2;
|
|
9122
|
+
var SETTLE = 150;
|
|
9123
|
+
var ESCAPE = 30;
|
|
9124
|
+
var TRY_WIDTH = 1280;
|
|
9125
|
+
var LOOKAHEAD = 2;
|
|
9126
|
+
function blank(key, searching) {
|
|
9127
|
+
return {
|
|
9128
|
+
key,
|
|
9129
|
+
groups: [],
|
|
9130
|
+
sources: [],
|
|
9131
|
+
queue: [],
|
|
9132
|
+
seen: new Set,
|
|
9133
|
+
checked: 0,
|
|
9134
|
+
total: 0,
|
|
9135
|
+
focus: 0,
|
|
9136
|
+
top: 0,
|
|
9137
|
+
searching
|
|
9138
|
+
};
|
|
9139
|
+
}
|
|
9140
|
+
function describe(error) {
|
|
9141
|
+
if (!(error instanceof Error)) {
|
|
9142
|
+
return String(error);
|
|
9143
|
+
}
|
|
9144
|
+
if (error instanceof AggregateError && error.errors[0]) {
|
|
9145
|
+
return describe(error.errors[0]);
|
|
9146
|
+
}
|
|
9147
|
+
if (error.cause) {
|
|
9148
|
+
return describe(error.cause);
|
|
9149
|
+
}
|
|
9150
|
+
const code = error.code;
|
|
9151
|
+
if (typeof code === "string") {
|
|
9152
|
+
return code;
|
|
9153
|
+
}
|
|
9154
|
+
const first = (error.message || error.name).split(".")[0] ?? "";
|
|
9155
|
+
return first.length > 56 ? `${first.slice(0, 55)}…` : first;
|
|
9156
|
+
}
|
|
9157
|
+
function reason(site, error) {
|
|
9158
|
+
return `${site.name}: ${describe(error)}`;
|
|
9159
|
+
}
|
|
9160
|
+
function incomplete(input) {
|
|
9161
|
+
const at2 = input.lastIndexOf("\x1B");
|
|
9162
|
+
return at2 !== -1 && /^\[?[0-9;]*$/.test(input.slice(at2 + 1));
|
|
9163
|
+
}
|
|
9164
|
+
function readCache(site, name) {
|
|
9165
|
+
try {
|
|
9166
|
+
return JSON.parse(readFileSync6(join7(cacheDir(site), name), "utf8"));
|
|
9167
|
+
} catch {
|
|
9168
|
+
return {};
|
|
9169
|
+
}
|
|
9170
|
+
}
|
|
9171
|
+
function writeCache(site, name, data) {
|
|
9172
|
+
mkdirSync5(cacheDir(site), { recursive: true });
|
|
9173
|
+
writeFileSync5(join7(cacheDir(site), name), `${JSON.stringify(data)}
|
|
9174
|
+
`);
|
|
9175
|
+
}
|
|
9176
|
+
|
|
9177
|
+
class Finder {
|
|
8208
9178
|
view;
|
|
8209
9179
|
tone;
|
|
8210
9180
|
session = new AbortController;
|
|
@@ -8213,19 +9183,28 @@ class Finder {
|
|
|
8213
9183
|
rows = process.stdout.rows || 24;
|
|
8214
9184
|
gen = 0;
|
|
8215
9185
|
pumping = 0;
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
seen = new Set;
|
|
9186
|
+
boards = new Map;
|
|
9187
|
+
board;
|
|
8219
9188
|
posts = new Map;
|
|
9189
|
+
thumbPath = new Map;
|
|
8220
9190
|
siteIndex = 0;
|
|
9191
|
+
values = new Map(SETTINGS.map((setting) => [setting.name, initial(setting, process.env[setting.name])]));
|
|
9192
|
+
direction = 1;
|
|
9193
|
+
prefetching = 0;
|
|
9194
|
+
inflight = new Map;
|
|
8221
9195
|
owners = new Map;
|
|
9196
|
+
probed = new Map;
|
|
9197
|
+
unsaved = new Set;
|
|
8222
9198
|
thumbQueue = [];
|
|
8223
9199
|
thumbing = 0;
|
|
9200
|
+
clarity = new Map;
|
|
9201
|
+
renders = new Renderer;
|
|
8224
9202
|
sent = new Map;
|
|
8225
9203
|
placed = new Map;
|
|
8226
9204
|
current;
|
|
8227
9205
|
fetch;
|
|
8228
9206
|
settle;
|
|
9207
|
+
partial;
|
|
8229
9208
|
input = "";
|
|
8230
9209
|
probeWait;
|
|
8231
9210
|
done;
|
|
@@ -8245,17 +9224,32 @@ class Finder {
|
|
|
8245
9224
|
site: this.site.name,
|
|
8246
9225
|
siteAnsi: this.site.ansi,
|
|
8247
9226
|
nextSite: this.nextSite.name,
|
|
8248
|
-
preset: "cutouts",
|
|
9227
|
+
preset: this.setting("TTHEME_FIND_POSTS") === "all" ? "all" : "cutouts",
|
|
9228
|
+
order: this.setting("TTHEME_FIND_ORDER") === "score" ? "score" : "newest",
|
|
9229
|
+
rating: ratingSet(this.setting("TTHEME_FIND_RATING")),
|
|
9230
|
+
block: blockSet(this.setting("TTHEME_FIND_BLOCK")),
|
|
9231
|
+
sets: this.setting("TTHEME_FIND_SETS") === "show" ? "show" : "fold",
|
|
9232
|
+
unblocked: process.env.TTHEME_FIND_PROXY !== undefined,
|
|
9233
|
+
settings: SETTINGS.map((setting) => ({
|
|
9234
|
+
label: setting.label,
|
|
9235
|
+
choices: [...setting.choices],
|
|
9236
|
+
value: this.setting(setting.name),
|
|
9237
|
+
multi: setting.multi && { none: setting.multi.none },
|
|
9238
|
+
cursor: 0
|
|
9239
|
+
})),
|
|
8249
9240
|
colors: { cursor: entry.cursor, selection: entry.selection, ansi: entry.ansi },
|
|
8250
9241
|
tiles: [],
|
|
8251
9242
|
checked: 0,
|
|
8252
9243
|
total: 0,
|
|
8253
|
-
searching:
|
|
9244
|
+
searching: tag !== "",
|
|
8254
9245
|
focus: 0,
|
|
8255
9246
|
top: 0,
|
|
8256
9247
|
mode: "grid",
|
|
8257
|
-
help: false
|
|
9248
|
+
help: false,
|
|
9249
|
+
editing: tag === "" ? "" : undefined
|
|
8258
9250
|
};
|
|
9251
|
+
this.board = blank(this.boardKey, tag !== "");
|
|
9252
|
+
this.boards.set(this.board.key, this.board);
|
|
8259
9253
|
}
|
|
8260
9254
|
get signal() {
|
|
8261
9255
|
return this.session.signal;
|
|
@@ -8266,6 +9260,52 @@ class Finder {
|
|
|
8266
9260
|
get nextSite() {
|
|
8267
9261
|
return SITES[(this.siteIndex + 1) % SITES.length];
|
|
8268
9262
|
}
|
|
9263
|
+
setting(name) {
|
|
9264
|
+
return this.values.get(name);
|
|
9265
|
+
}
|
|
9266
|
+
get boardKey() {
|
|
9267
|
+
return `${this.site.key}|${this.view.preset}|${this.view.order}`;
|
|
9268
|
+
}
|
|
9269
|
+
mark(site, id) {
|
|
9270
|
+
return `${site.key}:${id}`;
|
|
9271
|
+
}
|
|
9272
|
+
origPath(site, id, ext) {
|
|
9273
|
+
return join7(cacheDir(site), "orig", `${id}.${ext}`);
|
|
9274
|
+
}
|
|
9275
|
+
tileOf(site, post2, variants) {
|
|
9276
|
+
const version2 = rendition(post2) ?? post2;
|
|
9277
|
+
return {
|
|
9278
|
+
id: post2.id,
|
|
9279
|
+
width: version2.width,
|
|
9280
|
+
height: version2.height,
|
|
9281
|
+
reduced: version2 !== post2,
|
|
9282
|
+
owner: mirrored(post2.owner) ? "" : post2.owner,
|
|
9283
|
+
artist: post2.artist,
|
|
9284
|
+
score: post2.score,
|
|
9285
|
+
variants,
|
|
9286
|
+
origin: originHost(post2.source),
|
|
9287
|
+
mates: this.owners.get(site.key)?.get(post2.owner) ?? [],
|
|
9288
|
+
thumb: this.thumbPath.get(this.mark(site, post2.id))
|
|
9289
|
+
};
|
|
9290
|
+
}
|
|
9291
|
+
show() {
|
|
9292
|
+
const site = this.site;
|
|
9293
|
+
const view = this.view;
|
|
9294
|
+
const board = this.board;
|
|
9295
|
+
view.tiles = board.groups.flatMap((group) => group.open ? group.posts.map((post2) => this.tileOf(site, post2, 0)) : [this.tileOf(site, group.posts[0], group.posts.length)]);
|
|
9296
|
+
view.checked = board.checked;
|
|
9297
|
+
view.total = board.total;
|
|
9298
|
+
view.searching = board.searching;
|
|
9299
|
+
view.note = board.note;
|
|
9300
|
+
view.focus = Math.max(0, Math.min(board.focus, view.tiles.length - 1));
|
|
9301
|
+
view.top = board.top;
|
|
9302
|
+
}
|
|
9303
|
+
syncSite() {
|
|
9304
|
+
const view = this.view;
|
|
9305
|
+
view.site = this.site.name;
|
|
9306
|
+
view.siteAnsi = this.site.ansi;
|
|
9307
|
+
view.nextSite = this.nextSite.name;
|
|
9308
|
+
}
|
|
8269
9309
|
async run() {
|
|
8270
9310
|
const { stdin, stdout } = process;
|
|
8271
9311
|
stdin.setRawMode(true);
|
|
@@ -8276,10 +9316,13 @@ class Finder {
|
|
|
8276
9316
|
const exit = new Promise((resolve2) => {
|
|
8277
9317
|
this.done = resolve2;
|
|
8278
9318
|
});
|
|
9319
|
+
const clock = setInterval(this.onClock, 250);
|
|
8279
9320
|
const cell = await this.probe();
|
|
8280
9321
|
if (cell) {
|
|
8281
9322
|
this.cell = cell;
|
|
8282
|
-
this.
|
|
9323
|
+
if (this.view.tag) {
|
|
9324
|
+
this.search();
|
|
9325
|
+
}
|
|
8283
9326
|
} else {
|
|
8284
9327
|
this.view.searching = false;
|
|
8285
9328
|
this.view.error = "this terminal does not report its cell size — find needs kitty graphics";
|
|
@@ -8289,12 +9332,17 @@ class Finder {
|
|
|
8289
9332
|
this.session.abort();
|
|
8290
9333
|
this.fetch?.abort();
|
|
8291
9334
|
clearTimeout(this.settle);
|
|
9335
|
+
clearTimeout(this.partial);
|
|
9336
|
+
clearInterval(clock);
|
|
8292
9337
|
stdin.off("data", this.onData);
|
|
8293
9338
|
stdout.off("resize", this.onResize);
|
|
8294
9339
|
this.write("\x1B_Ga=d,d=A,q=2\x1B\\\x1B[H\x1B[J");
|
|
8295
9340
|
stdin.setRawMode(false);
|
|
8296
9341
|
stdin.pause();
|
|
8297
|
-
|
|
9342
|
+
this.saveProbes();
|
|
9343
|
+
this.renders.close();
|
|
9344
|
+
rmSync5(this.scratch, { recursive: true, force: true });
|
|
9345
|
+
sweepCache();
|
|
8298
9346
|
return code;
|
|
8299
9347
|
}
|
|
8300
9348
|
write(text) {
|
|
@@ -8328,18 +9376,34 @@ class Finder {
|
|
|
8328
9376
|
this.input = this.input.slice(0, at2) + this.input.slice(at2 + 1 + m[0].length);
|
|
8329
9377
|
this.probeWait({ h: Number(m[1]), w: Number(m[2]) });
|
|
8330
9378
|
}
|
|
9379
|
+
clearTimeout(this.partial);
|
|
9380
|
+
if (incomplete(this.input)) {
|
|
9381
|
+
this.partial = setTimeout(this.flushKeys, ESCAPE);
|
|
9382
|
+
return;
|
|
9383
|
+
}
|
|
9384
|
+
this.flushKeys();
|
|
9385
|
+
};
|
|
9386
|
+
flushKeys = () => {
|
|
8331
9387
|
const keys = decodeKeys(this.input);
|
|
8332
9388
|
this.input = "";
|
|
8333
9389
|
for (const key of keys) {
|
|
8334
9390
|
this.key(key);
|
|
8335
9391
|
}
|
|
8336
9392
|
};
|
|
9393
|
+
onClock = () => {
|
|
9394
|
+
const left = Math.ceil((pausedUntil(this.site) - Date.now()) / 1000);
|
|
9395
|
+
const waiting = left > 0 ? left : undefined;
|
|
9396
|
+
if (waiting !== this.view.waiting) {
|
|
9397
|
+
this.view.waiting = waiting;
|
|
9398
|
+
this.draw();
|
|
9399
|
+
}
|
|
9400
|
+
};
|
|
8337
9401
|
onResize = () => {
|
|
8338
9402
|
this.cols = process.stdout.columns || this.cols;
|
|
8339
9403
|
this.rows = process.stdout.rows || this.rows;
|
|
8340
9404
|
this.scroll();
|
|
8341
9405
|
if (this.view.mode === "try" && this.current) {
|
|
8342
|
-
this.
|
|
9406
|
+
this.reveal();
|
|
8343
9407
|
}
|
|
8344
9408
|
this.pump();
|
|
8345
9409
|
this.draw();
|
|
@@ -8353,6 +9417,14 @@ class Finder {
|
|
|
8353
9417
|
if (view.installing !== undefined) {
|
|
8354
9418
|
return;
|
|
8355
9419
|
}
|
|
9420
|
+
if (view.editing !== undefined) {
|
|
9421
|
+
this.editKey(key);
|
|
9422
|
+
return;
|
|
9423
|
+
}
|
|
9424
|
+
if (view.panel !== undefined) {
|
|
9425
|
+
this.panelKey(key);
|
|
9426
|
+
return;
|
|
9427
|
+
}
|
|
8356
9428
|
if (view.help) {
|
|
8357
9429
|
if (key === "?" || key === "esc") {
|
|
8358
9430
|
view.help = false;
|
|
@@ -8401,29 +9473,265 @@ class Finder {
|
|
|
8401
9473
|
this.select();
|
|
8402
9474
|
return;
|
|
8403
9475
|
}
|
|
8404
|
-
if (key === "
|
|
9476
|
+
if (key === "c") {
|
|
8405
9477
|
view.preset = view.preset === "cutouts" ? "all" : "cutouts";
|
|
8406
|
-
this.
|
|
9478
|
+
this.turn();
|
|
8407
9479
|
return;
|
|
8408
9480
|
}
|
|
8409
|
-
if (key === "
|
|
9481
|
+
if (key === "tab") {
|
|
8410
9482
|
this.siteIndex = (this.siteIndex + 1) % SITES.length;
|
|
8411
|
-
this.
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
9483
|
+
this.syncSite();
|
|
9484
|
+
this.turn();
|
|
9485
|
+
return;
|
|
9486
|
+
}
|
|
9487
|
+
if (key === "/") {
|
|
9488
|
+
view.editing = "";
|
|
9489
|
+
this.draw();
|
|
9490
|
+
return;
|
|
9491
|
+
}
|
|
9492
|
+
if (key === " ") {
|
|
9493
|
+
this.unfold();
|
|
9494
|
+
return;
|
|
9495
|
+
}
|
|
9496
|
+
if (key === "o") {
|
|
9497
|
+
this.openPage();
|
|
9498
|
+
return;
|
|
9499
|
+
}
|
|
9500
|
+
if (key === "s") {
|
|
9501
|
+
view.panel = 0;
|
|
9502
|
+
this.draw();
|
|
8417
9503
|
return;
|
|
8418
9504
|
}
|
|
8419
9505
|
if (key === "esc") {
|
|
8420
9506
|
this.finish(2);
|
|
8421
9507
|
}
|
|
8422
9508
|
}
|
|
9509
|
+
panelKey(key) {
|
|
9510
|
+
const view = this.view;
|
|
9511
|
+
const at2 = view.panel ?? 0;
|
|
9512
|
+
const row = view.settings[at2];
|
|
9513
|
+
if (key === "up" || key === "down") {
|
|
9514
|
+
view.panel = Math.max(0, Math.min(view.settings.length - 1, at2 + (key === "up" ? -1 : 1)));
|
|
9515
|
+
this.draw();
|
|
9516
|
+
return;
|
|
9517
|
+
}
|
|
9518
|
+
if ((key === "left" || key === "right") && row) {
|
|
9519
|
+
const step = key === "left" ? -1 : 1;
|
|
9520
|
+
if (row.multi) {
|
|
9521
|
+
row.cursor = (row.cursor + step + row.choices.length) % row.choices.length;
|
|
9522
|
+
} else {
|
|
9523
|
+
const index = row.choices.indexOf(row.value);
|
|
9524
|
+
row.value = row.choices[(index + step + row.choices.length) % row.choices.length];
|
|
9525
|
+
}
|
|
9526
|
+
this.draw();
|
|
9527
|
+
return;
|
|
9528
|
+
}
|
|
9529
|
+
if (key === " " && row?.multi) {
|
|
9530
|
+
const on = row.value.split(" ");
|
|
9531
|
+
const next = row.choices.filter((choice, k) => k === row.cursor ? !on.includes(choice) : on.includes(choice));
|
|
9532
|
+
const empty = row.multi.none;
|
|
9533
|
+
if (next.length > 0 || empty !== undefined) {
|
|
9534
|
+
row.value = next.length > 0 ? next.join(" ") : empty;
|
|
9535
|
+
this.draw();
|
|
9536
|
+
}
|
|
9537
|
+
return;
|
|
9538
|
+
}
|
|
9539
|
+
if (key === "esc") {
|
|
9540
|
+
view.settings.forEach((setting, i) => {
|
|
9541
|
+
setting.value = this.setting(SETTINGS[i]?.name ?? "");
|
|
9542
|
+
});
|
|
9543
|
+
view.panel = undefined;
|
|
9544
|
+
this.draw();
|
|
9545
|
+
return;
|
|
9546
|
+
}
|
|
9547
|
+
if (key === "enter" || key === "alt-c") {
|
|
9548
|
+
view.panel = undefined;
|
|
9549
|
+
this.adopt();
|
|
9550
|
+
}
|
|
9551
|
+
}
|
|
9552
|
+
adopt() {
|
|
9553
|
+
const view = this.view;
|
|
9554
|
+
const before = SETTINGS.map((setting) => this.setting(setting.name));
|
|
9555
|
+
view.settings.forEach((row, i) => {
|
|
9556
|
+
const setting = SETTINGS[i];
|
|
9557
|
+
if (setting) {
|
|
9558
|
+
this.values.set(setting.name, row.value);
|
|
9559
|
+
}
|
|
9560
|
+
});
|
|
9561
|
+
const changed = SETTINGS.filter((setting, i) => this.setting(setting.name) !== before[i]);
|
|
9562
|
+
if (changed.length === 0) {
|
|
9563
|
+
this.draw();
|
|
9564
|
+
return;
|
|
9565
|
+
}
|
|
9566
|
+
this.save(changed);
|
|
9567
|
+
view.rating = ratingSet(this.setting("TTHEME_FIND_RATING"));
|
|
9568
|
+
view.block = blockSet(this.setting("TTHEME_FIND_BLOCK"));
|
|
9569
|
+
view.sets = this.setting("TTHEME_FIND_SETS") === "show" ? "show" : "fold";
|
|
9570
|
+
view.preset = this.setting("TTHEME_FIND_POSTS") === "all" ? "all" : "cutouts";
|
|
9571
|
+
view.order = this.setting("TTHEME_FIND_ORDER") === "score" ? "score" : "newest";
|
|
9572
|
+
this.boards.clear();
|
|
9573
|
+
this.current = undefined;
|
|
9574
|
+
view.shown = undefined;
|
|
9575
|
+
view.mode = "grid";
|
|
9576
|
+
this.search();
|
|
9577
|
+
}
|
|
9578
|
+
save(changed) {
|
|
9579
|
+
const path2 = join7(this.home, "ttheme", "config.zsh");
|
|
9580
|
+
try {
|
|
9581
|
+
const before = existsSync3(path2) ? readFileSync6(path2, "utf8") : "";
|
|
9582
|
+
const after = changed.reduce((text, setting) => withSetting(text, setting.name, this.setting(setting.name)), before);
|
|
9583
|
+
writeFileSync5(path2, after);
|
|
9584
|
+
} catch (error) {
|
|
9585
|
+
this.view.error = error instanceof Error ? error.message : String(error);
|
|
9586
|
+
}
|
|
9587
|
+
}
|
|
9588
|
+
editKey(key) {
|
|
9589
|
+
const view = this.view;
|
|
9590
|
+
const text = view.editing ?? "";
|
|
9591
|
+
if (key === "esc") {
|
|
9592
|
+
view.editing = undefined;
|
|
9593
|
+
if (view.tag === "") {
|
|
9594
|
+
this.finish(2);
|
|
9595
|
+
return;
|
|
9596
|
+
}
|
|
9597
|
+
this.draw();
|
|
9598
|
+
return;
|
|
9599
|
+
}
|
|
9600
|
+
if (key === "enter") {
|
|
9601
|
+
view.editing = undefined;
|
|
9602
|
+
if (text.trim()) {
|
|
9603
|
+
this.commit(text.trim());
|
|
9604
|
+
return;
|
|
9605
|
+
}
|
|
9606
|
+
if (view.tag === "") {
|
|
9607
|
+
this.finish(2);
|
|
9608
|
+
return;
|
|
9609
|
+
}
|
|
9610
|
+
this.draw();
|
|
9611
|
+
return;
|
|
9612
|
+
}
|
|
9613
|
+
if (key === "backspace") {
|
|
9614
|
+
view.editing = text.slice(0, -1);
|
|
9615
|
+
this.draw();
|
|
9616
|
+
return;
|
|
9617
|
+
}
|
|
9618
|
+
if (key.length === 1 && key >= " ") {
|
|
9619
|
+
view.editing = text + key;
|
|
9620
|
+
this.draw();
|
|
9621
|
+
}
|
|
9622
|
+
}
|
|
9623
|
+
async commit(text) {
|
|
9624
|
+
const ref = postRef(text, this.site);
|
|
9625
|
+
if (ref) {
|
|
9626
|
+
await this.jump(ref.site, ref.id);
|
|
9627
|
+
return;
|
|
9628
|
+
}
|
|
9629
|
+
this.view.tag = text;
|
|
9630
|
+
this.boards.clear();
|
|
9631
|
+
await this.search();
|
|
9632
|
+
}
|
|
9633
|
+
async jump(site, id) {
|
|
9634
|
+
const gen = ++this.gen;
|
|
9635
|
+
this.siteIndex = SITES.indexOf(site);
|
|
9636
|
+
this.syncSite();
|
|
9637
|
+
this.current = undefined;
|
|
9638
|
+
this.view.shown = undefined;
|
|
9639
|
+
this.view.error = undefined;
|
|
9640
|
+
const board = blank(`${site.key}|post:${id}`, true);
|
|
9641
|
+
this.board = board;
|
|
9642
|
+
this.boards.set(board.key, board);
|
|
9643
|
+
this.show();
|
|
9644
|
+
this.draw();
|
|
9645
|
+
try {
|
|
9646
|
+
const post2 = await fetchPost(site, id, this.signal);
|
|
9647
|
+
if (gen !== this.gen) {
|
|
9648
|
+
return;
|
|
9649
|
+
}
|
|
9650
|
+
board.searching = false;
|
|
9651
|
+
if (!post2) {
|
|
9652
|
+
board.error = `${site.name} has no post ${id}`;
|
|
9653
|
+
this.view.error = board.error;
|
|
9654
|
+
} else {
|
|
9655
|
+
this.posts.set(this.mark(site, post2.id), post2);
|
|
9656
|
+
board.groups = [{ posts: [post2], open: false }];
|
|
9657
|
+
board.checked = 1;
|
|
9658
|
+
board.total = 1;
|
|
9659
|
+
this.thumbQueue.push({ site, post: post2 });
|
|
9660
|
+
this.thumbs();
|
|
9661
|
+
this.view.mode = "try";
|
|
9662
|
+
}
|
|
9663
|
+
this.show();
|
|
9664
|
+
if (post2) {
|
|
9665
|
+
this.select();
|
|
9666
|
+
}
|
|
9667
|
+
this.draw();
|
|
9668
|
+
} catch (error) {
|
|
9669
|
+
this.fail(gen, error);
|
|
9670
|
+
}
|
|
9671
|
+
}
|
|
9672
|
+
async turn() {
|
|
9673
|
+
this.current = undefined;
|
|
9674
|
+
this.view.shown = undefined;
|
|
9675
|
+
const known = this.boards.get(this.boardKey);
|
|
9676
|
+
if (!known) {
|
|
9677
|
+
await this.search();
|
|
9678
|
+
return;
|
|
9679
|
+
}
|
|
9680
|
+
this.gen++;
|
|
9681
|
+
this.board = known;
|
|
9682
|
+
this.view.error = known.error;
|
|
9683
|
+
this.show();
|
|
9684
|
+
this.draw();
|
|
9685
|
+
await this.pump();
|
|
9686
|
+
}
|
|
9687
|
+
unfold() {
|
|
9688
|
+
const view = this.view;
|
|
9689
|
+
let index = 0;
|
|
9690
|
+
for (const group of this.board.groups) {
|
|
9691
|
+
const size = group.open ? group.posts.length : 1;
|
|
9692
|
+
if (view.focus < index + size) {
|
|
9693
|
+
if (group.posts.length < 2) {
|
|
9694
|
+
return;
|
|
9695
|
+
}
|
|
9696
|
+
group.open = !group.open;
|
|
9697
|
+
this.board.focus = index;
|
|
9698
|
+
for (const post2 of group.posts) {
|
|
9699
|
+
if (!this.thumbPath.has(this.mark(this.site, post2.id))) {
|
|
9700
|
+
this.thumbQueue.push({ site: this.site, post: post2 });
|
|
9701
|
+
}
|
|
9702
|
+
}
|
|
9703
|
+
this.thumbs();
|
|
9704
|
+
this.show();
|
|
9705
|
+
this.scroll();
|
|
9706
|
+
this.draw();
|
|
9707
|
+
return;
|
|
9708
|
+
}
|
|
9709
|
+
index += size;
|
|
9710
|
+
}
|
|
9711
|
+
}
|
|
9712
|
+
openPage() {
|
|
9713
|
+
const tile = this.view.tiles[this.view.focus];
|
|
9714
|
+
if (!tile) {
|
|
9715
|
+
return;
|
|
9716
|
+
}
|
|
9717
|
+
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
9718
|
+
try {
|
|
9719
|
+
spawn2(opener, [this.site.pageUrl(tile.id)], { stdio: "ignore", detached: true }).unref();
|
|
9720
|
+
} catch {}
|
|
9721
|
+
}
|
|
8423
9722
|
tryKey(key) {
|
|
8424
9723
|
const view = this.view;
|
|
8425
9724
|
if (key === "left" || key === "right") {
|
|
8426
|
-
this.
|
|
9725
|
+
this.direction = key === "left" ? -1 : 1;
|
|
9726
|
+
this.focus(view.focus + this.direction);
|
|
9727
|
+
return;
|
|
9728
|
+
}
|
|
9729
|
+
if (key === "o") {
|
|
9730
|
+
this.openPage();
|
|
9731
|
+
return;
|
|
9732
|
+
}
|
|
9733
|
+
if (key === "x") {
|
|
9734
|
+
this.swap();
|
|
8427
9735
|
return;
|
|
8428
9736
|
}
|
|
8429
9737
|
if (key === "enter") {
|
|
@@ -8449,6 +9757,7 @@ class Finder {
|
|
|
8449
9757
|
return;
|
|
8450
9758
|
}
|
|
8451
9759
|
view.focus = next;
|
|
9760
|
+
this.board.focus = next;
|
|
8452
9761
|
this.scroll();
|
|
8453
9762
|
this.pump();
|
|
8454
9763
|
if (view.mode === "try") {
|
|
@@ -8466,135 +9775,217 @@ class Finder {
|
|
|
8466
9775
|
if (row > view.top + rowsVis - 1) {
|
|
8467
9776
|
view.top = row - rowsVis + 1;
|
|
8468
9777
|
}
|
|
9778
|
+
this.board.top = view.top;
|
|
9779
|
+
}
|
|
9780
|
+
query(site) {
|
|
9781
|
+
const view = this.view;
|
|
9782
|
+
const wanted = [
|
|
9783
|
+
view.tag,
|
|
9784
|
+
site.rate(this.view.rating),
|
|
9785
|
+
view.preset === "cutouts" ? site.cutouts : "",
|
|
9786
|
+
view.order === "score" ? site.best : ""
|
|
9787
|
+
].filter(Boolean);
|
|
9788
|
+
const kept = [];
|
|
9789
|
+
const dropped = [];
|
|
9790
|
+
for (const part of wanted) {
|
|
9791
|
+
if (tagsOf([...kept, part].join(" ")) <= site.tagBudget) {
|
|
9792
|
+
kept.push(part);
|
|
9793
|
+
} else {
|
|
9794
|
+
dropped.push(part);
|
|
9795
|
+
}
|
|
9796
|
+
}
|
|
9797
|
+
this.board.note = dropped.length > 0 ? `${site.name} takes ${site.tagBudget} tags — ${dropped.join(" ")} left out` : undefined;
|
|
9798
|
+
return kept.join(" ");
|
|
8469
9799
|
}
|
|
8470
9800
|
async search() {
|
|
8471
9801
|
const gen = ++this.gen;
|
|
8472
|
-
const view = this.view;
|
|
8473
|
-
Object.assign(view, { tiles: [], checked: 0, total: 0, searching: true, focus: 0, top: 0, error: undefined });
|
|
8474
|
-
this.seen.clear();
|
|
8475
|
-
this.queue = [];
|
|
8476
|
-
this.sources = [];
|
|
8477
|
-
this.thumbQueue = [];
|
|
8478
|
-
this.draw();
|
|
8479
9802
|
const site = this.site;
|
|
8480
|
-
const
|
|
9803
|
+
const board = blank(this.boardKey, true);
|
|
9804
|
+
this.board = board;
|
|
9805
|
+
this.boards.set(board.key, board);
|
|
9806
|
+
this.view.error = undefined;
|
|
9807
|
+
this.show();
|
|
9808
|
+
this.draw();
|
|
9809
|
+
const tags = this.query(site);
|
|
9810
|
+
this.tally(gen, site, board, tags);
|
|
8481
9811
|
try {
|
|
8482
|
-
const
|
|
9812
|
+
const owners = await this.mateOwners(site);
|
|
8483
9813
|
if (gen !== this.gen) {
|
|
8484
9814
|
return;
|
|
8485
9815
|
}
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
...[...owners.keys()].map((owner) => ({ tags: `${tags} user:${owner}`, page: 0, done: false })),
|
|
9816
|
+
const room = tagsOf(tags) + 1 <= site.tagBudget;
|
|
9817
|
+
board.sources = [
|
|
9818
|
+
...room ? [...owners.keys()].map((owner) => ({ tags: `${tags} user:${owner}`, page: 0, done: false })) : [],
|
|
8489
9819
|
{ tags, page: 0, done: false }
|
|
8490
9820
|
];
|
|
9821
|
+
this.show();
|
|
8491
9822
|
this.draw();
|
|
8492
9823
|
await this.pump();
|
|
8493
9824
|
} catch (error) {
|
|
8494
9825
|
this.fail(gen, error);
|
|
8495
9826
|
}
|
|
8496
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
|
+
}
|
|
8497
9838
|
fail(gen, error) {
|
|
8498
9839
|
if (gen !== this.gen || this.signal.aborted) {
|
|
8499
9840
|
return;
|
|
8500
9841
|
}
|
|
8501
|
-
this.
|
|
8502
|
-
this.
|
|
9842
|
+
this.board.error = reason(this.site, error);
|
|
9843
|
+
this.board.searching = false;
|
|
9844
|
+
this.view.error = this.board.error;
|
|
9845
|
+
this.show();
|
|
8503
9846
|
this.draw();
|
|
8504
9847
|
}
|
|
8505
9848
|
wants() {
|
|
8506
|
-
const view = this.view;
|
|
8507
|
-
if (view.checked < Math.min(view.total, PAGE)) {
|
|
8508
|
-
return true;
|
|
8509
|
-
}
|
|
8510
9849
|
const { perRow, rowsVis } = gridShape(this.cols, this.rows);
|
|
8511
|
-
return view.tiles.length < (
|
|
9850
|
+
return this.view.tiles.length < (this.board.top + rowsVis + LOOKAHEAD) * perRow;
|
|
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
|
+
}
|
|
8512
9869
|
}
|
|
8513
9870
|
async pump() {
|
|
8514
9871
|
const gen = this.gen;
|
|
8515
|
-
|
|
9872
|
+
const board = this.board;
|
|
9873
|
+
if (this.pumping === gen || board.sources.length === 0) {
|
|
8516
9874
|
return;
|
|
8517
9875
|
}
|
|
8518
9876
|
this.pumping = gen;
|
|
8519
|
-
const
|
|
9877
|
+
const site = this.site;
|
|
9878
|
+
const flight = [];
|
|
9879
|
+
let page;
|
|
8520
9880
|
try {
|
|
8521
|
-
|
|
8522
|
-
while (gen === this.gen
|
|
8523
|
-
|
|
8524
|
-
const
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
const
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
}
|
|
8532
|
-
source.page++;
|
|
8533
|
-
source.done = posts.length < PAGE;
|
|
8534
|
-
for (const post2 of posts) {
|
|
8535
|
-
if (!this.seen.has(post2.id)) {
|
|
8536
|
-
this.seen.add(post2.id);
|
|
8537
|
-
this.queue.push(post2);
|
|
9881
|
+
board.searching = true;
|
|
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;
|
|
8538
9891
|
}
|
|
9892
|
+
});
|
|
9893
|
+
job.catch(() => {});
|
|
9894
|
+
page = job;
|
|
9895
|
+
}
|
|
9896
|
+
const head = flight.shift();
|
|
9897
|
+
if (!head) {
|
|
9898
|
+
if (!page) {
|
|
9899
|
+
break;
|
|
8539
9900
|
}
|
|
9901
|
+
await page;
|
|
8540
9902
|
continue;
|
|
8541
9903
|
}
|
|
8542
|
-
const
|
|
8543
|
-
const passed = await Promise.all(batch.map((post2) => this.passes(post2)));
|
|
9904
|
+
const passed = await head.passed;
|
|
8544
9905
|
if (gen !== this.gen) {
|
|
8545
9906
|
return;
|
|
8546
9907
|
}
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
});
|
|
9908
|
+
board.checked++;
|
|
9909
|
+
if (passed) {
|
|
9910
|
+
this.admit(site, head.post);
|
|
9911
|
+
}
|
|
9912
|
+
this.show();
|
|
8553
9913
|
this.draw();
|
|
8554
9914
|
}
|
|
8555
9915
|
if (gen === this.gen) {
|
|
8556
|
-
|
|
9916
|
+
board.searching = false;
|
|
9917
|
+
this.show();
|
|
8557
9918
|
this.draw();
|
|
8558
9919
|
}
|
|
8559
9920
|
} catch (error) {
|
|
8560
9921
|
this.fail(gen, error);
|
|
8561
9922
|
} finally {
|
|
9923
|
+
this.saveProbes();
|
|
8562
9924
|
if (this.pumping === gen) {
|
|
8563
9925
|
this.pumping = 0;
|
|
8564
9926
|
}
|
|
8565
9927
|
}
|
|
8566
9928
|
}
|
|
8567
|
-
async passes(post2) {
|
|
8568
|
-
|
|
9929
|
+
async passes(site, post2) {
|
|
9930
|
+
const version2 = rendition(post2);
|
|
9931
|
+
if (!rated(site, post2, this.view.rating) || exposed(post2, this.view.block).length > 0 || !version2) {
|
|
8569
9932
|
return false;
|
|
8570
9933
|
}
|
|
8571
9934
|
if (this.view.preset === "all") {
|
|
8572
|
-
return ["png", "jpg", "jpeg"].includes(
|
|
9935
|
+
return ["png", "jpg", "jpeg"].includes(version2.ext);
|
|
8573
9936
|
}
|
|
8574
9937
|
if (post2.ext !== "png") {
|
|
8575
9938
|
return false;
|
|
8576
9939
|
}
|
|
8577
|
-
|
|
8578
|
-
|
|
9940
|
+
return site.vouched || this.transparent(site, post2);
|
|
9941
|
+
}
|
|
9942
|
+
probes(site) {
|
|
9943
|
+
let known = this.probed.get(site.key);
|
|
9944
|
+
if (!known) {
|
|
9945
|
+
known = readCache(site, "probes.json");
|
|
9946
|
+
this.probed.set(site.key, known);
|
|
9947
|
+
}
|
|
9948
|
+
return known;
|
|
9949
|
+
}
|
|
9950
|
+
async transparent(site, post2) {
|
|
9951
|
+
const known = this.probes(site);
|
|
9952
|
+
const cached = known[post2.id];
|
|
9953
|
+
if (cached !== undefined) {
|
|
9954
|
+
return cached;
|
|
8579
9955
|
}
|
|
8580
9956
|
try {
|
|
8581
|
-
|
|
9957
|
+
const alpha = (await headOf(site, post2.file, this.signal))?.alpha === true;
|
|
9958
|
+
known[post2.id] = alpha;
|
|
9959
|
+
this.unsaved.add(site.key);
|
|
9960
|
+
return alpha;
|
|
8582
9961
|
} catch {
|
|
8583
9962
|
return false;
|
|
8584
9963
|
}
|
|
8585
9964
|
}
|
|
8586
|
-
|
|
8587
|
-
const
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
this.
|
|
8597
|
-
this.
|
|
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
|
+
}
|
|
9971
|
+
}
|
|
9972
|
+
this.unsaved.clear();
|
|
9973
|
+
}
|
|
9974
|
+
admit(site, post2) {
|
|
9975
|
+
this.posts.set(this.mark(site, post2.id), post2);
|
|
9976
|
+
const last = this.view.sets === "fold" ? this.board.groups.at(-1) : undefined;
|
|
9977
|
+
const head = last?.posts[0];
|
|
9978
|
+
const credit = post2.owner || post2.artist;
|
|
9979
|
+
if (last && head && credit && (head.owner || head.artist) === credit && head.width === post2.width && head.height === post2.height) {
|
|
9980
|
+
last.posts.push(post2);
|
|
9981
|
+
if (last.open) {
|
|
9982
|
+
this.thumbQueue.push({ site, post: post2 });
|
|
9983
|
+
this.thumbs();
|
|
9984
|
+
}
|
|
9985
|
+
return;
|
|
9986
|
+
}
|
|
9987
|
+
this.board.groups.push({ posts: [post2], open: false });
|
|
9988
|
+
this.thumbQueue.push({ site, post: post2 });
|
|
8598
9989
|
this.thumbs();
|
|
8599
9990
|
}
|
|
8600
9991
|
async mateOwners(site) {
|
|
@@ -8603,70 +9994,76 @@ class Finder {
|
|
|
8603
9994
|
return cached;
|
|
8604
9995
|
}
|
|
8605
9996
|
const found = origins(this.home);
|
|
8606
|
-
const
|
|
8607
|
-
|
|
8608
|
-
try {
|
|
8609
|
-
known = JSON.parse(readFileSync4(cachePath, "utf8"));
|
|
8610
|
-
} catch {}
|
|
8611
|
-
const byPalette = new Map;
|
|
8612
|
-
for (const sibling of this.catalog.palettes) {
|
|
9997
|
+
const known = readCache(site, "owners.json");
|
|
9998
|
+
const siblings = this.catalog.palettes.flatMap((sibling) => {
|
|
8613
9999
|
const origin = found.get(sibling.name);
|
|
8614
|
-
|
|
8615
|
-
|
|
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 };
|
|
8616
10006
|
}
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
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);
|
|
8625
10018
|
}
|
|
8626
|
-
byPalette.set(sibling.name, owner);
|
|
8627
10019
|
}
|
|
8628
|
-
|
|
8629
|
-
writeFileSync4(cachePath, `${JSON.stringify(known)}
|
|
8630
|
-
`);
|
|
10020
|
+
writeCache(site, "owners.json", known);
|
|
8631
10021
|
const owners = mates(byPalette);
|
|
8632
10022
|
this.owners.set(site.key, owners);
|
|
8633
10023
|
return owners;
|
|
8634
10024
|
}
|
|
8635
|
-
async cached(site, path2, url) {
|
|
10025
|
+
async cached(site, path2, url, progress2) {
|
|
8636
10026
|
if (existsSync3(path2)) {
|
|
8637
|
-
return
|
|
10027
|
+
return statSync3(path2).size;
|
|
10028
|
+
}
|
|
10029
|
+
const running = this.inflight.get(path2);
|
|
10030
|
+
if (running) {
|
|
10031
|
+
return running;
|
|
10032
|
+
}
|
|
10033
|
+
const job = (async () => {
|
|
10034
|
+
const bytes = await fetchBytes(site, url, this.signal, progress2);
|
|
10035
|
+
mkdirSync5(dirname4(path2), { recursive: true });
|
|
10036
|
+
writeFileSync5(path2, bytes);
|
|
10037
|
+
return bytes.length;
|
|
10038
|
+
})();
|
|
10039
|
+
this.inflight.set(path2, job);
|
|
10040
|
+
try {
|
|
10041
|
+
return await job;
|
|
10042
|
+
} finally {
|
|
10043
|
+
this.inflight.delete(path2);
|
|
8638
10044
|
}
|
|
8639
|
-
const bytes = await fetchBytes(site, url, this.signal);
|
|
8640
|
-
mkdirSync4(dirname3(path2), { recursive: true });
|
|
8641
|
-
writeFileSync4(path2, bytes);
|
|
8642
|
-
return bytes;
|
|
8643
10045
|
}
|
|
8644
10046
|
thumbs() {
|
|
8645
|
-
while (this.thumbing <
|
|
8646
|
-
const
|
|
10047
|
+
while (this.thumbing < THUMB && this.thumbQueue.length > 0) {
|
|
10048
|
+
const { site, post: post2 } = this.thumbQueue.shift();
|
|
8647
10049
|
this.thumbing++;
|
|
8648
|
-
this.thumb(
|
|
10050
|
+
this.thumb(site, post2).catch(() => {}).finally(() => {
|
|
8649
10051
|
this.thumbing--;
|
|
8650
10052
|
this.thumbs();
|
|
8651
10053
|
});
|
|
8652
10054
|
}
|
|
8653
10055
|
}
|
|
8654
|
-
async thumb(
|
|
8655
|
-
const post2 = this.posts.get(tile.id);
|
|
8656
|
-
const site = this.site;
|
|
8657
|
-
if (!post2) {
|
|
8658
|
-
return;
|
|
8659
|
-
}
|
|
10056
|
+
async thumb(site, post2) {
|
|
8660
10057
|
const w = TILE.cols * this.cell.w;
|
|
8661
10058
|
const h = TILE.rows * this.cell.h;
|
|
8662
|
-
const path2 = join7(cacheDir(site), "tile", `${
|
|
10059
|
+
const path2 = join7(cacheDir(site), "tile", `${post2.id}-${w}x${h}.png`);
|
|
8663
10060
|
if (!existsSync3(path2)) {
|
|
8664
|
-
const thumb = join7(cacheDir(site), "thumb", `${
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
writeFileSync4(path2, encodePng(contain(decodeImage(bytes), w, h)));
|
|
10061
|
+
const thumb = join7(cacheDir(site), "thumb", `${post2.id}.${extension(post2.preview)}`);
|
|
10062
|
+
await this.cached(site, thumb, post2.preview);
|
|
10063
|
+
await this.renders.run({ job: "thumb", from: thumb, to: path2, width: w, height: h });
|
|
8668
10064
|
}
|
|
8669
|
-
|
|
10065
|
+
this.thumbPath.set(this.mark(site, post2.id), path2);
|
|
10066
|
+
this.show();
|
|
8670
10067
|
this.draw();
|
|
8671
10068
|
}
|
|
8672
10069
|
select() {
|
|
@@ -8679,8 +10076,7 @@ class Finder {
|
|
|
8679
10076
|
return;
|
|
8680
10077
|
}
|
|
8681
10078
|
if (this.current?.id === tile.id) {
|
|
8682
|
-
this.
|
|
8683
|
-
this.draw();
|
|
10079
|
+
this.reveal();
|
|
8684
10080
|
return;
|
|
8685
10081
|
}
|
|
8686
10082
|
this.settle = setTimeout(() => void this.load(tile), SETTLE);
|
|
@@ -8688,46 +10084,74 @@ class Finder {
|
|
|
8688
10084
|
}
|
|
8689
10085
|
async load(tile) {
|
|
8690
10086
|
const view = this.view;
|
|
8691
|
-
const post2 = this.posts.get(tile.id);
|
|
8692
|
-
|
|
10087
|
+
const post2 = this.posts.get(this.mark(this.site, tile.id));
|
|
10088
|
+
const version2 = post2 && rendition(post2);
|
|
10089
|
+
if (!version2) {
|
|
8693
10090
|
return;
|
|
8694
10091
|
}
|
|
8695
10092
|
const site = this.site;
|
|
8696
10093
|
const control = new AbortController;
|
|
8697
10094
|
this.fetch = control;
|
|
8698
|
-
const signal = AbortSignal.any([this.signal, control.signal]);
|
|
8699
10095
|
try {
|
|
8700
|
-
const path2 =
|
|
8701
|
-
|
|
8702
|
-
if (existsSync3(path2)) {
|
|
8703
|
-
bytes = new Uint8Array(readFileSync4(path2));
|
|
8704
|
-
} else {
|
|
10096
|
+
const path2 = this.origPath(site, tile.id, version2.ext);
|
|
10097
|
+
if (!existsSync3(path2)) {
|
|
8705
10098
|
view.fetching = { id: tile.id, got: 0, size: 0 };
|
|
8706
10099
|
this.draw();
|
|
8707
|
-
bytes = await fetchBytes(site, post2.file, signal, (got, size) => {
|
|
8708
|
-
if (view.fetching?.id === tile.id) {
|
|
8709
|
-
view.fetching = { id: tile.id, got, size };
|
|
8710
|
-
this.draw();
|
|
8711
|
-
}
|
|
8712
|
-
});
|
|
8713
|
-
mkdirSync4(dirname3(path2), { recursive: true });
|
|
8714
|
-
writeFileSync4(path2, bytes);
|
|
8715
10100
|
}
|
|
10101
|
+
const size = await this.cached(site, path2, version2.file, (got, total) => {
|
|
10102
|
+
if (view.fetching?.id === tile.id) {
|
|
10103
|
+
view.fetching = { id: tile.id, got, size: total };
|
|
10104
|
+
this.draw();
|
|
10105
|
+
}
|
|
10106
|
+
});
|
|
8716
10107
|
if (control.signal.aborted) {
|
|
8717
10108
|
return;
|
|
8718
10109
|
}
|
|
8719
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 };
|
|
8720
10115
|
view.preparing = tile.id;
|
|
8721
10116
|
this.flush();
|
|
8722
|
-
await
|
|
8723
|
-
|
|
8724
|
-
if (site !== this.site) {
|
|
10117
|
+
const plain2 = await this.render(current, "plain");
|
|
10118
|
+
if (control.signal.aborted || site !== this.site) {
|
|
8725
10119
|
return;
|
|
8726
10120
|
}
|
|
8727
|
-
|
|
10121
|
+
if (plain2.clear === 0 && canRemoveBackground() && this.setting("TTHEME_FIND_REMOVE_BG") !== "off") {
|
|
10122
|
+
view.preparing = undefined;
|
|
10123
|
+
view.cutting = tile.id;
|
|
10124
|
+
this.flush();
|
|
10125
|
+
current.cut = await this.cutOut(site, tile.id, path2, control.signal);
|
|
10126
|
+
if (control.signal.aborted || site !== this.site) {
|
|
10127
|
+
return;
|
|
10128
|
+
}
|
|
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
|
+
}
|
|
10146
|
+
} else {
|
|
10147
|
+
current.failed = true;
|
|
10148
|
+
}
|
|
10149
|
+
}
|
|
10150
|
+
this.current = current;
|
|
8728
10151
|
if (view.tiles[view.focus]?.id === tile.id) {
|
|
8729
|
-
this.present();
|
|
10152
|
+
await this.present();
|
|
8730
10153
|
}
|
|
10154
|
+
this.preload();
|
|
8731
10155
|
} catch (error) {
|
|
8732
10156
|
if (!control.signal.aborted && !this.signal.aborted) {
|
|
8733
10157
|
view.error = reason(site, error);
|
|
@@ -8739,23 +10163,110 @@ class Finder {
|
|
|
8739
10163
|
if (view.preparing === tile.id) {
|
|
8740
10164
|
view.preparing = undefined;
|
|
8741
10165
|
}
|
|
10166
|
+
if (view.cutting === tile.id) {
|
|
10167
|
+
view.cutting = undefined;
|
|
10168
|
+
}
|
|
8742
10169
|
this.draw();
|
|
8743
10170
|
}
|
|
8744
10171
|
}
|
|
8745
|
-
|
|
10172
|
+
async cutOut(site, id, path2, signal) {
|
|
10173
|
+
const out = join7(cacheDir(site), "cut", `${id}.png`);
|
|
10174
|
+
if (existsSync3(out)) {
|
|
10175
|
+
return out;
|
|
10176
|
+
}
|
|
10177
|
+
try {
|
|
10178
|
+
mkdirSync5(dirname4(out), { recursive: true });
|
|
10179
|
+
await removeBackground(path2, out, signal);
|
|
10180
|
+
return out;
|
|
10181
|
+
} catch {
|
|
10182
|
+
rmSync5(out, { force: true });
|
|
10183
|
+
return;
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
10186
|
+
swap() {
|
|
8746
10187
|
const current = this.current;
|
|
8747
|
-
if (!current) {
|
|
10188
|
+
if (!current?.cut || this.view.shown?.id !== current.id) {
|
|
10189
|
+
return;
|
|
10190
|
+
}
|
|
10191
|
+
current.using = current.using === "cut" ? "plain" : "cut";
|
|
10192
|
+
this.reveal();
|
|
10193
|
+
}
|
|
10194
|
+
preload() {
|
|
10195
|
+
const view = this.view;
|
|
10196
|
+
if (view.mode !== "try") {
|
|
8748
10197
|
return;
|
|
8749
10198
|
}
|
|
10199
|
+
const site = this.site;
|
|
10200
|
+
for (const index of [view.focus + this.direction, view.focus - this.direction]) {
|
|
10201
|
+
if (this.prefetching >= PRELOAD) {
|
|
10202
|
+
return;
|
|
10203
|
+
}
|
|
10204
|
+
const tile = view.tiles[index];
|
|
10205
|
+
const post2 = tile && this.posts.get(this.mark(site, tile.id));
|
|
10206
|
+
const version2 = post2 && rendition(post2);
|
|
10207
|
+
if (!version2) {
|
|
10208
|
+
continue;
|
|
10209
|
+
}
|
|
10210
|
+
const path2 = this.origPath(site, post2.id, version2.ext);
|
|
10211
|
+
this.prefetching++;
|
|
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(() => {
|
|
10213
|
+
this.prefetching--;
|
|
10214
|
+
});
|
|
10215
|
+
}
|
|
10216
|
+
}
|
|
10217
|
+
async render(current, using) {
|
|
8750
10218
|
const W = this.cols * this.cell.w;
|
|
8751
10219
|
const H = this.rows * this.cell.h;
|
|
8752
10220
|
const width2 = Math.min(W, TRY_WIDTH);
|
|
8753
10221
|
const height = Math.max(1, Math.round(H * width2 / W));
|
|
8754
|
-
const path2 = join7(this.scratch, `${current.id}-${width2}x${height}.png`);
|
|
8755
|
-
|
|
8756
|
-
|
|
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;
|
|
8757
10249
|
}
|
|
8758
|
-
this.view.shown = {
|
|
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
|
+
});
|
|
8759
10270
|
}
|
|
8760
10271
|
async install() {
|
|
8761
10272
|
const view = this.view;
|
|
@@ -8767,15 +10278,24 @@ class Finder {
|
|
|
8767
10278
|
view.installing = tile.id;
|
|
8768
10279
|
view.error = undefined;
|
|
8769
10280
|
this.flush();
|
|
8770
|
-
await tick();
|
|
8771
10281
|
try {
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
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
|
+
}
|
|
8778
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);
|
|
8779
10299
|
process.stderr.write(`background · ${this.entry.name} ← ${current.site.name} ${current.id}
|
|
8780
10300
|
`);
|
|
8781
10301
|
this.finish(0);
|
|
@@ -8825,17 +10345,64 @@ class Finder {
|
|
|
8825
10345
|
this.write(`${out}\x1B[?2026l`);
|
|
8826
10346
|
}
|
|
8827
10347
|
}
|
|
10348
|
+
function routable() {
|
|
10349
|
+
if (process.versions.bun) {
|
|
10350
|
+
return true;
|
|
10351
|
+
}
|
|
10352
|
+
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
10353
|
+
return major > 22 || major === 22 && minor >= 21;
|
|
10354
|
+
}
|
|
10355
|
+
async function relaunch() {
|
|
10356
|
+
if (!routable()) {
|
|
10357
|
+
process.stderr.write(`unblock needs node 22.21 or newer — this is ${process.versions.node}
|
|
10358
|
+
`);
|
|
10359
|
+
return 1;
|
|
10360
|
+
}
|
|
10361
|
+
const proxy = await tunnel();
|
|
10362
|
+
const child = spawn2(process.execPath, process.argv.slice(1), {
|
|
10363
|
+
stdio: "inherit",
|
|
10364
|
+
env: {
|
|
10365
|
+
...process.env,
|
|
10366
|
+
NODE_USE_ENV_PROXY: "1",
|
|
10367
|
+
NODE_NO_WARNINGS: "1",
|
|
10368
|
+
HTTPS_PROXY: `http://127.0.0.1:${proxy.port}`,
|
|
10369
|
+
TTHEME_FIND_PROXY: String(proxy.port)
|
|
10370
|
+
}
|
|
10371
|
+
});
|
|
10372
|
+
const code = await new Promise((resolve2) => child.on("exit", (status2) => resolve2(status2 ?? 1)));
|
|
10373
|
+
proxy.close();
|
|
10374
|
+
return code;
|
|
10375
|
+
}
|
|
8828
10376
|
async function runFind(name) {
|
|
10377
|
+
if (process.env.TTHEME_FIND_UNBLOCK === "1" && !process.env.TTHEME_FIND_PROXY) {
|
|
10378
|
+
return relaunch();
|
|
10379
|
+
}
|
|
8829
10380
|
const home = configHome();
|
|
8830
10381
|
const catalog = readCatalog(home);
|
|
8831
10382
|
const entry = find2(catalog.palettes, name);
|
|
8832
|
-
if (!entry.booru) {
|
|
8833
|
-
throw new Error(`${name} has no booru tag to search for`);
|
|
8834
|
-
}
|
|
8835
10383
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
8836
10384
|
throw new Error("ttheme find needs a terminal");
|
|
8837
10385
|
}
|
|
8838
|
-
return new Finder(home, catalog, entry, entry.booru).run();
|
|
10386
|
+
return new Finder(home, catalog, entry, entry.booru ?? "").run();
|
|
10387
|
+
}
|
|
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`);
|
|
8839
10406
|
}
|
|
8840
10407
|
|
|
8841
10408
|
// src/init.ts
|
|
@@ -8843,14 +10410,14 @@ import {
|
|
|
8843
10410
|
chmodSync,
|
|
8844
10411
|
copyFileSync,
|
|
8845
10412
|
existsSync as existsSync5,
|
|
8846
|
-
mkdirSync as
|
|
8847
|
-
readdirSync as
|
|
8848
|
-
readFileSync as
|
|
8849
|
-
rmSync as
|
|
8850
|
-
writeFileSync as
|
|
10413
|
+
mkdirSync as mkdirSync6,
|
|
10414
|
+
readdirSync as readdirSync4,
|
|
10415
|
+
readFileSync as readFileSync7,
|
|
10416
|
+
rmSync as rmSync6,
|
|
10417
|
+
writeFileSync as writeFileSync6
|
|
8851
10418
|
} from "node:fs";
|
|
8852
10419
|
import { homedir as homedir3 } from "node:os";
|
|
8853
|
-
import { dirname as
|
|
10420
|
+
import { dirname as dirname5, join as join8 } from "node:path";
|
|
8854
10421
|
|
|
8855
10422
|
// node_modules/@clack/core/dist/index.mjs
|
|
8856
10423
|
import { styleText } from "node:util";
|
|
@@ -10789,12 +12356,12 @@ async function runBrowse() {
|
|
|
10789
12356
|
|
|
10790
12357
|
// src/init.ts
|
|
10791
12358
|
function copyDir(copies, from, to) {
|
|
10792
|
-
for (const f2 of
|
|
12359
|
+
for (const f2 of readdirSync4(from)) {
|
|
10793
12360
|
copies.push({ from: join8(from, f2), to: join8(to, f2) });
|
|
10794
12361
|
}
|
|
10795
12362
|
}
|
|
10796
12363
|
function loadManifest(root2) {
|
|
10797
|
-
return JSON.parse(
|
|
12364
|
+
return JSON.parse(readFileSync7(join8(root2, "dist", "manifest.json"), "utf8"));
|
|
10798
12365
|
}
|
|
10799
12366
|
function planInit(opts, paths) {
|
|
10800
12367
|
const dist = join8(paths.root, "dist");
|
|
@@ -10810,7 +12377,7 @@ function planInit(opts, paths) {
|
|
|
10810
12377
|
const configPath = join8(home, "config.zsh");
|
|
10811
12378
|
const settings2 = {
|
|
10812
12379
|
file: configPath,
|
|
10813
|
-
content: configFile(existsSync5(configPath) ?
|
|
12380
|
+
content: configFile(existsSync5(configPath) ? readFileSync7(configPath, "utf8") : "")
|
|
10814
12381
|
};
|
|
10815
12382
|
const notes = [];
|
|
10816
12383
|
if (opts.terminals.includes("ghostty")) {
|
|
@@ -10818,7 +12385,7 @@ function planInit(opts, paths) {
|
|
|
10818
12385
|
}
|
|
10819
12386
|
if (opts.terminals.includes("alacritty")) {
|
|
10820
12387
|
const config = join8(paths.configHome, "alacritty", "alacritty.toml");
|
|
10821
|
-
if (existsSync5(config) && !
|
|
12388
|
+
if (existsSync5(config) && !readFileSync7(config, "utf8").includes("# ttheme begin")) {
|
|
10822
12389
|
notes.push("alacritty.toml already exists — ttheme left it alone; add its themes/ import yourself");
|
|
10823
12390
|
}
|
|
10824
12391
|
}
|
|
@@ -10828,23 +12395,23 @@ function planInit(opts, paths) {
|
|
|
10828
12395
|
}
|
|
10829
12396
|
function applyInit(plan) {
|
|
10830
12397
|
for (const c2 of plan.copies) {
|
|
10831
|
-
|
|
10832
|
-
|
|
12398
|
+
mkdirSync6(dirname5(c2.to), { recursive: true });
|
|
12399
|
+
rmSync6(c2.to, { force: true });
|
|
10833
12400
|
copyFileSync(c2.from, c2.to);
|
|
10834
12401
|
if (c2.executable) {
|
|
10835
12402
|
chmodSync(c2.to, 493);
|
|
10836
12403
|
}
|
|
10837
12404
|
}
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
const configHome2 =
|
|
12405
|
+
mkdirSync6(dirname5(plan.settings.file), { recursive: true });
|
|
12406
|
+
writeFileSync6(plan.settings.file, plan.settings.content);
|
|
12407
|
+
const configHome2 = dirname5(dirname5(plan.settings.file));
|
|
10841
12408
|
writeCatalog(configHome2, plan.catalog);
|
|
10842
12409
|
writeInstalled(configHome2, plan.installed);
|
|
10843
12410
|
sync(configHome2, plan.catalog, plan.installed);
|
|
10844
12411
|
for (const e of plan.edits) {
|
|
10845
|
-
|
|
10846
|
-
const current = existsSync5(e.file) ?
|
|
10847
|
-
|
|
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));
|
|
10848
12415
|
}
|
|
10849
12416
|
}
|
|
10850
12417
|
function accepted(value) {
|
|
@@ -10866,7 +12433,7 @@ function verify(plan) {
|
|
|
10866
12433
|
const missing = [
|
|
10867
12434
|
...plan.copies.filter((c2) => !existsSync5(c2.to)).map((c2) => c2.to),
|
|
10868
12435
|
...existsSync5(plan.settings.file) ? [] : [plan.settings.file],
|
|
10869
|
-
...plan.edits.filter((e) => !
|
|
12436
|
+
...plan.edits.filter((e) => !readFileSync7(e.file, "utf8").includes("# ttheme begin")).map((e) => e.file)
|
|
10870
12437
|
];
|
|
10871
12438
|
if (missing.length > 0) {
|
|
10872
12439
|
throw new Error(`init left gaps:
|
|
@@ -10991,6 +12558,9 @@ function createProgram() {
|
|
|
10991
12558
|
program2.command("find", { hidden: true }).argument("<palette>").description("pick a safebooru background for a palette — preview opens this on tab").action(async (name) => {
|
|
10992
12559
|
process.exitCode = await runFind(name);
|
|
10993
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
|
+
});
|
|
10994
12564
|
program2.command("update").description("refresh the catalog from the registry").action(() => runUpdate());
|
|
10995
12565
|
return program2;
|
|
10996
12566
|
}
|
|
@@ -11009,4 +12579,6 @@ ${error instanceof Error ? error.message : String(error)}`);
|
|
|
11009
12579
|
}
|
|
11010
12580
|
|
|
11011
12581
|
// src/bin.ts
|
|
11012
|
-
|
|
12582
|
+
if (isMainThread2) {
|
|
12583
|
+
process.exitCode = await runCli(process.argv);
|
|
12584
|
+
}
|