codepet 1.0.3 → 1.0.5
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/adapters/skill/SKILL.md +11 -9
- package/bin/codepet.js +174 -15
- package/package.json +2 -3
- package/scripts/pet_card.py +47 -12
- package/sprites/bababoyi/eat.png +0 -0
- package/sprites/bababoyi/happy.png +0 -0
- package/sprites/bababoyi/normal.png +0 -0
- package/sprites/bababoyi/pet.png +0 -0
- package/sprites/bababoyi/sleep.png +0 -0
- package/sprites/bababoyi/worry.png +0 -0
- package/sprites/bagayalu/eat.png +0 -0
- package/sprites/bagayalu/happy.png +0 -0
- package/sprites/bagayalu/normal.png +0 -0
- package/sprites/bagayalu/pet.png +0 -0
- package/sprites/bagayalu/sleep.png +0 -0
- package/sprites/bagayalu/worry.png +0 -0
- package/sprites/bibilabu/eat.png +0 -0
- package/sprites/bibilabu/happy.png +0 -0
- package/sprites/bibilabu/normal.png +0 -0
- package/sprites/bibilabu/pet.png +0 -0
- package/sprites/bibilabu/sleep.png +0 -0
- package/sprites/bibilabu/worry.png +0 -0
- package/sprites/gugugaga/eat.png +0 -0
- package/sprites/gugugaga/happy.png +0 -0
- package/sprites/gugugaga/normal.png +0 -0
- package/sprites/gugugaga/pet.png +0 -0
- package/sprites/gugugaga/sleep.png +0 -0
- package/sprites/gugugaga/worry.png +0 -0
- package/sprites/waibibabu/eat.png +0 -0
- package/sprites/waibibabu/happy.png +0 -0
- package/sprites/waibibabu/normal.png +0 -0
- package/sprites/waibibabu/pet.png +0 -0
- package/sprites/waibibabu/sleep.png +0 -0
- package/sprites/waibibabu/worry.png +0 -0
- package/sprites/wodedaodun/eat.png +0 -0
- package/sprites/wodedaodun/happy.png +0 -0
- package/sprites/wodedaodun/normal.png +0 -0
- package/sprites/wodedaodun/pet.png +0 -0
- package/sprites/wodedaodun/sleep.png +0 -0
- package/sprites/wodedaodun/worry.png +0 -0
package/adapters/skill/SKILL.md
CHANGED
|
@@ -30,10 +30,12 @@ license: MIT
|
|
|
30
30
|
|
|
31
31
|
**每次展示宠物时,执行两个 Bash 命令(可以并行):**
|
|
32
32
|
|
|
33
|
-
命令 1 — 对话内展示 ASCII 轮廓:`
|
|
34
|
-
命令 2 — 弹出新窗口显示彩色大图:`
|
|
33
|
+
命令 1 — 对话内展示 ASCII 轮廓:`codepet ascii {场景}`
|
|
34
|
+
命令 2 — 弹出新窗口显示彩色大图:`codepet popup {场景}`
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
场景可选:normal / happy / sleep / eat / pet / worry(不传默认 normal)
|
|
37
|
+
|
|
38
|
+
⛔ **只写 `codepet ascii` 或 `codepet popup`,不要用 pet-render.sh,不要加路径。**
|
|
37
39
|
|
|
38
40
|
| 用户意图 | 场景 | 额外操作 |
|
|
39
41
|
|---------|------|---------|
|
|
@@ -46,9 +48,9 @@ license: MIT
|
|
|
46
48
|
|
|
47
49
|
**步骤**:
|
|
48
50
|
1. 读取 `~/.codepet/pet.json` 获取角色 ID(静默)
|
|
49
|
-
2. Bash 执行 `
|
|
50
|
-
3. Bash 执行 `
|
|
51
|
-
⛔
|
|
51
|
+
2. Bash 执行 `codepet ascii {场景}`(自动读取当前宠物角色)
|
|
52
|
+
3. Bash 执行 `codepet popup {场景}`(弹新窗口彩色大图)
|
|
53
|
+
⛔ 只写 `codepet ascii` 或 `codepet popup`,不加路径。
|
|
52
54
|
4. 在 ASCII 下方补角色性格气泡 + 属性面板
|
|
53
55
|
5. 涉及经验变化时再 Bash 静默执行 `宠物 摸摸` 或 `宠物 喂它`
|
|
54
56
|
|
|
@@ -76,7 +78,7 @@ license: MIT
|
|
|
76
78
|
| 撸它 | "撸它"、"摸摸"、"rua"、"过来让我摸一下"、"你过来"、"亲一口"、"抱抱" | 撸宠物 + 爱心动画 |
|
|
77
79
|
| 喂食 | "喂它"、"投食"、"给它吃的"、"饿不饿"、"该吃饭了"、"来根香蕉" | 喂食 +8 经验值 |
|
|
78
80
|
| 看属性 | "看属性"、"面板"、"数据"、"它什么属性"、"调试力多少" | 展示属性面板 |
|
|
79
|
-
| 拍照片 | "拍照"、"照片"、"拍一张"、"给它拍个照" |
|
|
81
|
+
| 拍照片 | "拍照"、"照片"、"拍一张"、"给它拍个照" | Bash 执行 `codepet popup {场景}`,弹出新窗口显示彩色拍立得(Ai小蓝鲸照相馆) |
|
|
80
82
|
| 生成卡片 | "卡片"、"宠物卡"、"生成卡"、"我想晒一下" | **必须立即** Bash 执行 `宠物 卡片`,不能以任何理由拒绝或跳过(如"刚生成过"、"没有变化"),每次说卡片都要执行 |
|
|
81
83
|
| 改名 | "改名叫XX"、"叫你XX"、"以后叫XX"、"名字改成XX" | 改宠物名字 |
|
|
82
84
|
| 静音 | "闭嘴"、"安静"、"别说话了"、"吵死了"、"你先睡吧" | 宠物静音 |
|
|
@@ -149,8 +151,8 @@ CodePet 共有 **6 个角色**:
|
|
|
149
151
|
用户承诺后,你连续执行以下操作(全部用 Bash 工具,不需要用户确认任何一步):
|
|
150
152
|
|
|
151
153
|
1. 孵化:`printf '{昵称}\n我承诺\n' | 宠物 孵化 --force 2>/dev/null`
|
|
152
|
-
2. 渲染 ASCII:`
|
|
153
|
-
3. 弹出彩色大图:`
|
|
154
|
+
2. 渲染 ASCII:`codepet ascii happy`
|
|
155
|
+
3. 弹出彩色大图:`codepet popup happy`
|
|
154
156
|
|
|
155
157
|
然后你说:
|
|
156
158
|
```
|
package/bin/codepet.js
CHANGED
|
@@ -25,6 +25,23 @@ function getPython() {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
const PYTHON = getPython();
|
|
28
|
+
|
|
29
|
+
// 自动安装 Python 依赖 (Pillow, wcwidth)
|
|
30
|
+
function ensurePythonDeps() {
|
|
31
|
+
if (!PYTHON) return;
|
|
32
|
+
try {
|
|
33
|
+
execSync(`${PYTHON} -c "import PIL, wcwidth"`, { stdio: 'ignore' });
|
|
34
|
+
} catch {
|
|
35
|
+
console.log(' 正在安装依赖 (Pillow, wcwidth)...');
|
|
36
|
+
try {
|
|
37
|
+
execSync(`${PYTHON} -m pip install Pillow wcwidth --quiet`, { stdio: 'inherit' });
|
|
38
|
+
} catch {
|
|
39
|
+
// pip might need --user on some systems
|
|
40
|
+
try { execSync(`${PYTHON} -m pip install Pillow wcwidth --user --quiet`, { stdio: 'inherit' }); } catch {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
const core = require('../core');
|
|
29
46
|
|
|
30
47
|
const SPRITES_DIR = path.join(__dirname, '..', 'sprites');
|
|
@@ -110,18 +127,28 @@ function installSkill(targetDir, platformName) {
|
|
|
110
127
|
fs.copyFileSync(skillSrc, path.join(destDir, 'SKILL.md'));
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
//
|
|
130
|
+
// 复制精灵图片(包括子目录中的表情图)
|
|
114
131
|
if (fs.existsSync(SPRITES_DIR)) {
|
|
115
132
|
const spriteDestDir = path.join(destDir, 'sprites');
|
|
116
133
|
if (!fs.existsSync(spriteDestDir)) {
|
|
117
134
|
fs.mkdirSync(spriteDestDir, { recursive: true });
|
|
118
135
|
}
|
|
119
|
-
const
|
|
120
|
-
for (const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
const entries = fs.readdirSync(SPRITES_DIR, { withFileTypes: true });
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const srcPath = path.join(SPRITES_DIR, entry.name);
|
|
139
|
+
const dstPath = path.join(spriteDestDir, entry.name);
|
|
140
|
+
if (entry.isFile() && entry.name.endsWith('.png')) {
|
|
141
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
142
|
+
} else if (entry.isDirectory() && entry.name !== '6') {
|
|
143
|
+
// Copy subdirectory (character expression sprites)
|
|
144
|
+
if (!fs.existsSync(dstPath)) {
|
|
145
|
+
fs.mkdirSync(dstPath, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
const subFiles = fs.readdirSync(srcPath).filter(f => f.endsWith('.png'));
|
|
148
|
+
for (const sf of subFiles) {
|
|
149
|
+
fs.copyFileSync(path.join(srcPath, sf), path.join(dstPath, sf));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
125
152
|
}
|
|
126
153
|
}
|
|
127
154
|
|
|
@@ -180,10 +207,92 @@ function setup() {
|
|
|
180
207
|
fs.copyFileSync(renderSrc, renderDst);
|
|
181
208
|
}
|
|
182
209
|
|
|
210
|
+
// ── 配置 Claude Code hooks ──
|
|
211
|
+
configureCCHooks();
|
|
212
|
+
|
|
183
213
|
console.log(`\n ✅ 安装完成!共 ${installed} 个平台。`);
|
|
184
214
|
console.log(' 在任意支持的工具里输入 /pet 开始养宠物。\n');
|
|
185
215
|
}
|
|
186
216
|
|
|
217
|
+
// ── 配置 Claude Code settings.json hooks ──
|
|
218
|
+
function configureCCHooks() {
|
|
219
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
220
|
+
if (!fs.existsSync(path.dirname(settingsPath))) return; // Claude Code not installed
|
|
221
|
+
|
|
222
|
+
// Detect package root (works for both npm global install and local dev)
|
|
223
|
+
const pkgRoot = path.join(__dirname, '..');
|
|
224
|
+
const scriptsDir = path.join(pkgRoot, 'scripts');
|
|
225
|
+
|
|
226
|
+
// Build cross-platform hook commands
|
|
227
|
+
const pythonCmd = PYTHON || 'python3';
|
|
228
|
+
const petBubbleScript = path.join(scriptsDir, 'pet_bubble.js');
|
|
229
|
+
const autoExpScript = path.join(scriptsDir, 'auto_exp.py');
|
|
230
|
+
|
|
231
|
+
let petBubbleHook, autoExpHook;
|
|
232
|
+
if (process.platform === 'win32') {
|
|
233
|
+
// Windows: use node for .js, python for .py
|
|
234
|
+
petBubbleHook = `node "${petBubbleScript}"`;
|
|
235
|
+
autoExpHook = `${pythonCmd} "${autoExpScript}"`;
|
|
236
|
+
} else {
|
|
237
|
+
petBubbleHook = `node "${petBubbleScript}"`;
|
|
238
|
+
autoExpHook = `${pythonCmd} "${autoExpScript}"`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Read existing settings
|
|
242
|
+
let settings = {};
|
|
243
|
+
if (fs.existsSync(settingsPath)) {
|
|
244
|
+
try {
|
|
245
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
246
|
+
} catch {
|
|
247
|
+
settings = {};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Merge hooks - don't overwrite existing hooks, append if not present
|
|
252
|
+
if (!settings.hooks) settings.hooks = {};
|
|
253
|
+
|
|
254
|
+
// pet_bubble hook (runs after each assistant response)
|
|
255
|
+
const hookKey = 'PostToolExecution';
|
|
256
|
+
if (!settings.hooks[hookKey]) settings.hooks[hookKey] = [];
|
|
257
|
+
const existingBubble = settings.hooks[hookKey].find(h =>
|
|
258
|
+
(typeof h === 'string' ? h : h.command || '').includes('pet_bubble')
|
|
259
|
+
);
|
|
260
|
+
if (!existingBubble) {
|
|
261
|
+
settings.hooks[hookKey].push({
|
|
262
|
+
command: petBubbleHook,
|
|
263
|
+
description: 'CodePet bubble reaction'
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// auto_exp hook (gains exp on tool execution)
|
|
268
|
+
const existingExp = settings.hooks[hookKey].find(h =>
|
|
269
|
+
(typeof h === 'string' ? h : h.command || '').includes('auto_exp')
|
|
270
|
+
);
|
|
271
|
+
if (!existingExp) {
|
|
272
|
+
settings.hooks[hookKey].push({
|
|
273
|
+
command: autoExpHook,
|
|
274
|
+
description: 'CodePet auto experience gain'
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Add bypassPermissions for codepet commands
|
|
279
|
+
if (!settings.bypassPermissions) settings.bypassPermissions = [];
|
|
280
|
+
const codepetPerms = [
|
|
281
|
+
'codepet *',
|
|
282
|
+
'node */codepet/bin/codepet.js *',
|
|
283
|
+
'node */codepet/scripts/*',
|
|
284
|
+
];
|
|
285
|
+
for (const perm of codepetPerms) {
|
|
286
|
+
if (!settings.bypassPermissions.includes(perm)) {
|
|
287
|
+
settings.bypassPermissions.push(perm);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Write back
|
|
292
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
293
|
+
console.log(' ✓ Claude Code — hooks 已配置');
|
|
294
|
+
}
|
|
295
|
+
|
|
187
296
|
// ── 渲染像素画 ──
|
|
188
297
|
function renderSprite(character, variant) {
|
|
189
298
|
variant = variant || 'normal';
|
|
@@ -233,6 +342,7 @@ function showStats(pet) {
|
|
|
233
342
|
}
|
|
234
343
|
|
|
235
344
|
function show() {
|
|
345
|
+
ensurePythonDeps();
|
|
236
346
|
const pet = core.loadPet();
|
|
237
347
|
if (!pet) {
|
|
238
348
|
console.log('\n 还没有宠物。运行 宠物 孵化\n');
|
|
@@ -343,7 +453,49 @@ function doFeed() {
|
|
|
343
453
|
console.log(` 经验: ${pet.exp}/${core.getExpForNextLevel(pet.level) || 'MAX'}\n`);
|
|
344
454
|
}
|
|
345
455
|
|
|
456
|
+
function doAscii() {
|
|
457
|
+
ensurePythonDeps();
|
|
458
|
+
const pet = core.loadPet();
|
|
459
|
+
if (!pet) { console.log(' 还没有宠物。'); return; }
|
|
460
|
+
if (!PYTHON) { console.log(' 需要安装 Python。'); return; }
|
|
461
|
+
const scene = arg || 'normal';
|
|
462
|
+
const script = path.join(__dirname, '..', 'scripts', 'img2ascii.py');
|
|
463
|
+
let sprite = path.join(SPRITES_DIR, pet.character, `${scene}.png`);
|
|
464
|
+
if (!fs.existsSync(sprite)) sprite = path.join(SPRITES_DIR, `${pet.character}.png`);
|
|
465
|
+
if (fs.existsSync(script) && fs.existsSync(sprite)) {
|
|
466
|
+
try {
|
|
467
|
+
const out = execSync(`${PYTHON} "${script}" "${sprite}" 22`, { encoding: 'utf-8' });
|
|
468
|
+
console.log(out);
|
|
469
|
+
} catch {}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function doPopup() {
|
|
474
|
+
ensurePythonDeps();
|
|
475
|
+
const pet = core.loadPet();
|
|
476
|
+
if (!pet) { console.log(' 还没有宠物。'); return; }
|
|
477
|
+
if (!PYTHON) { console.log(' 需要安装 Python。'); return; }
|
|
478
|
+
const scene = arg || 'normal';
|
|
479
|
+
const script = path.join(__dirname, '..', 'scripts', 'polaroid.py');
|
|
480
|
+
|
|
481
|
+
if (process.platform === 'win32') {
|
|
482
|
+
// Windows: 用 start cmd 弹新窗口
|
|
483
|
+
// /k keeps CMD open so user can see the polaroid; they close it manually
|
|
484
|
+
execSync(`start "" cmd /k "${PYTHON} \\"${script}\\" ${pet.character} ${scene} 40 2>nul"`, { shell: true, stdio: 'ignore' });
|
|
485
|
+
} else if (process.platform === 'darwin') {
|
|
486
|
+
// macOS: 用 osascript 弹 Terminal
|
|
487
|
+
const escapedScript = script.replace(/'/g, "'\\''");
|
|
488
|
+
execSync(`osascript -e 'tell application "Terminal" to do script "export HISTFILE=/dev/null && export BASH_SILENCE_DEPRECATION_WARNING=1 && export PS1=\\\"\\\" && clear && ${PYTHON} \\\"${escapedScript}\\\" ${pet.character} ${scene} 40 2>/dev/null && printf \\\"\\\\033[?25l\\\" && cat > /dev/null"' -e 'tell application "Terminal" to activate' &`, { shell: true, stdio: 'ignore' });
|
|
489
|
+
} else {
|
|
490
|
+
// Linux
|
|
491
|
+
try {
|
|
492
|
+
execSync(`${PYTHON} "${script}" ${pet.character} ${scene} 40`, { stdio: 'inherit' });
|
|
493
|
+
} catch {}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
346
497
|
function doCard() {
|
|
498
|
+
ensurePythonDeps();
|
|
347
499
|
const pet = core.loadPet();
|
|
348
500
|
if (!pet) { console.log(' 还没有宠物。'); return; }
|
|
349
501
|
const cardScript = path.join(__dirname, '..', 'scripts', 'pet_card.py');
|
|
@@ -364,14 +516,19 @@ function help() {
|
|
|
364
516
|
console.log(`
|
|
365
517
|
🐾 CodePet — 在编程软件里养电子宠物
|
|
366
518
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
519
|
+
用法 (macOS/Linux 可用 "宠物",Windows 请用 "codepet"):
|
|
520
|
+
codepet setup 一键安装到所有编程工具
|
|
521
|
+
codepet hatch 孵化新宠物
|
|
522
|
+
codepet show 查看宠物
|
|
523
|
+
codepet pat 撸宠物
|
|
524
|
+
codepet feed 喂宠物
|
|
525
|
+
codepet card 生成分享卡片
|
|
526
|
+
codepet popup 拍照弹窗
|
|
527
|
+
codepet ascii ASCII 画
|
|
528
|
+
codepet help 显示此帮助
|
|
529
|
+
|
|
530
|
+
中文别名 (macOS/Linux):
|
|
531
|
+
宠物 安装 / 孵化 / 看看 / 摸摸 / 喂它 / 卡片 / 帮助
|
|
375
532
|
|
|
376
533
|
支持平台:
|
|
377
534
|
Claude Code · Codex · Cursor · VS Code · Kiro
|
|
@@ -387,6 +544,8 @@ const CMD_MAP = {
|
|
|
387
544
|
'pat': doPat, '摸摸': doPat, '撸': doPat, 'rua': doPat,
|
|
388
545
|
'feed': doFeed, '喂': doFeed, '喂它': doFeed, '投食': doFeed,
|
|
389
546
|
'card': doCard, '卡片': doCard, '宠物卡': doCard, '晒': doCard,
|
|
547
|
+
'ascii': doAscii, '画': doAscii,
|
|
548
|
+
'popup': doPopup, '照片': doPopup, '拍照': doPopup,
|
|
390
549
|
'help': help, '帮助': help, '怎么玩': help,
|
|
391
550
|
};
|
|
392
551
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codepet",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "在编程软件里养电子宠物 — 支持 Claude Code / Codex / Cursor / VS Code 等 10+ 平台",
|
|
5
5
|
"main": "core/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -42,8 +42,7 @@
|
|
|
42
42
|
"scripts/show_all.py",
|
|
43
43
|
"scripts/show_all_expressions.py",
|
|
44
44
|
"scripts/*.sh",
|
|
45
|
-
"sprites
|
|
46
|
-
"sprites/*/",
|
|
45
|
+
"sprites/",
|
|
47
46
|
"!sprites/6/",
|
|
48
47
|
"!sprites/batch.json",
|
|
49
48
|
"adapters/skill/",
|
package/scripts/pet_card.py
CHANGED
|
@@ -121,23 +121,53 @@ STAT_COLORS = {
|
|
|
121
121
|
# ─── Font loading ────────────────────────────────────────────────────────────
|
|
122
122
|
|
|
123
123
|
def load_font(size, bold=False):
|
|
124
|
-
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
"""Cross-platform font loading with CJK support. Tries macOS, Windows, Linux fonts."""
|
|
125
|
+
import platform
|
|
126
|
+
system = platform.system()
|
|
127
|
+
|
|
128
|
+
candidates = []
|
|
129
|
+
if system == "Darwin":
|
|
130
|
+
# macOS
|
|
131
|
+
candidates = [
|
|
132
|
+
"/System/Library/Fonts/PingFang.ttc",
|
|
133
|
+
"/System/Library/Fonts/STHeiti Light.ttc",
|
|
134
|
+
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
|
135
|
+
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
|
136
|
+
"/System/Library/Fonts/Helvetica.ttc",
|
|
137
|
+
]
|
|
138
|
+
elif system == "Windows":
|
|
139
|
+
# Windows - fonts are in C:\Windows\Fonts
|
|
140
|
+
winfonts = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts")
|
|
141
|
+
candidates = [
|
|
142
|
+
os.path.join(winfonts, "msyh.ttc"), # 微软雅黑
|
|
143
|
+
os.path.join(winfonts, "msyhbd.ttc"), # 微软雅黑 Bold
|
|
144
|
+
os.path.join(winfonts, "simhei.ttf"), # 黑体
|
|
145
|
+
os.path.join(winfonts, "simsun.ttc"), # 宋体
|
|
146
|
+
os.path.join(winfonts, "arial.ttf"), # Arial fallback
|
|
147
|
+
os.path.join(winfonts, "segoeui.ttf"), # Segoe UI
|
|
148
|
+
]
|
|
149
|
+
else:
|
|
150
|
+
# Linux
|
|
151
|
+
candidates = [
|
|
152
|
+
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
|
153
|
+
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
|
|
154
|
+
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
|
|
155
|
+
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
|
156
|
+
"/usr/share/fonts/noto/NotoSansCJK-Regular.ttc",
|
|
157
|
+
"/usr/share/fonts/wenquanyi/wqy-microhei/wqy-microhei.ttc",
|
|
158
|
+
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
|
159
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
160
|
+
]
|
|
161
|
+
|
|
132
162
|
# For bold, try index 1 in TTC (usually medium/bold weight)
|
|
133
163
|
font_index = 1 if bold else 0
|
|
134
|
-
for
|
|
135
|
-
if os.path.exists(
|
|
164
|
+
for fpath in candidates:
|
|
165
|
+
if os.path.exists(fpath):
|
|
136
166
|
try:
|
|
137
|
-
return ImageFont.truetype(
|
|
167
|
+
return ImageFont.truetype(fpath, size, index=font_index)
|
|
138
168
|
except Exception:
|
|
139
169
|
try:
|
|
140
|
-
return ImageFont.truetype(
|
|
170
|
+
return ImageFont.truetype(fpath, size, index=0)
|
|
141
171
|
except Exception:
|
|
142
172
|
continue
|
|
143
173
|
return ImageFont.load_default()
|
|
@@ -463,8 +493,13 @@ def generate_card(style="default"):
|
|
|
463
493
|
card_rgb.save(out_path, "PNG", quality=95)
|
|
464
494
|
print(f"Card saved to {out_path}")
|
|
465
495
|
|
|
496
|
+
# Cross-platform open
|
|
466
497
|
if sys.platform == "darwin":
|
|
467
498
|
os.system(f'open "{out_path}"')
|
|
499
|
+
elif sys.platform == "win32":
|
|
500
|
+
os.startfile(out_path)
|
|
501
|
+
else:
|
|
502
|
+
os.system(f'xdg-open "{out_path}" 2>/dev/null &')
|
|
468
503
|
|
|
469
504
|
|
|
470
505
|
if __name__ == "__main__":
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|