fangge 1.0.1 → 1.0.3
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 +2 -2
- package/dist/cli.js +284 -35
- package/package.json +2 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { render } from "ink";
|
|
5
5
|
|
|
6
6
|
// source/app.tsx
|
|
7
|
-
import { Box as
|
|
7
|
+
import { Box as Box5, Text as Text6, useApp, useInput as useInput2 } from "ink";
|
|
8
8
|
|
|
9
9
|
// source/data.ts
|
|
10
10
|
var profile = {
|
|
@@ -44,10 +44,31 @@ var experiences = [
|
|
|
44
44
|
}
|
|
45
45
|
];
|
|
46
46
|
var projects = [
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
{
|
|
48
|
+
name: "ListenHub",
|
|
49
|
+
description: "AI podcast platform",
|
|
50
|
+
href: "https://listenhub.ai"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "LH Skills",
|
|
54
|
+
description: "AI skills for Claude Code",
|
|
55
|
+
href: "https://github.com/marswaveai/skills"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "LH CLI",
|
|
59
|
+
description: "Command-line interface for ListenHub",
|
|
60
|
+
href: "https://github.com/marswaveai/listenhub-cli"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "LH SDK",
|
|
64
|
+
description: "JavaScript SDK For ListenHub",
|
|
65
|
+
href: "https://github.com/marswaveai/listenhub-sdk"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "fango-cli",
|
|
69
|
+
description: "This terminal card",
|
|
70
|
+
href: "https://github.com/0xFANGO/fango-cli"
|
|
71
|
+
}
|
|
51
72
|
];
|
|
52
73
|
var education = {
|
|
53
74
|
school: "Beijing Univ. of Posts & Telecom",
|
|
@@ -55,12 +76,34 @@ var education = {
|
|
|
55
76
|
from: "2014",
|
|
56
77
|
to: "2018"
|
|
57
78
|
};
|
|
79
|
+
var musicUrl = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
|
|
58
80
|
var links = [
|
|
59
81
|
{ label: "ListenHub", href: "https://listenhub.ai", display: "listenhub.ai" },
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
{
|
|
83
|
+
label: "LH Skills",
|
|
84
|
+
href: "https://github.com/marswaveai/skills",
|
|
85
|
+
display: "github.com/marswaveai/skills"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
label: "LH CLI",
|
|
89
|
+
href: "https://github.com/marswaveai/listenhub-cli",
|
|
90
|
+
display: "github.com/marswaveai/listenhub-cli"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: "LH SDK",
|
|
94
|
+
href: "https://github.com/marswaveai/listenhub-sdk",
|
|
95
|
+
display: "github.com/marswaveai/listenhub-sdk"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
label: "GitHub",
|
|
99
|
+
href: "https://github.com/0xFANGO",
|
|
100
|
+
display: "github.com/0xFANGO"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
label: "Email",
|
|
104
|
+
href: "mailto:silencerichard@163.com",
|
|
105
|
+
display: "silencerichard@163.com"
|
|
106
|
+
},
|
|
64
107
|
{ label: "Website", href: "https://fango.blog", display: "fango.blog" }
|
|
65
108
|
];
|
|
66
109
|
|
|
@@ -178,17 +221,218 @@ var Links = ({ links: links2 }) => {
|
|
|
178
221
|
] });
|
|
179
222
|
};
|
|
180
223
|
|
|
181
|
-
// source/
|
|
224
|
+
// source/components/music-player.tsx
|
|
225
|
+
import { Text as Text4 } from "ink";
|
|
182
226
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
227
|
+
var MusicPlayer = ({ status }) => {
|
|
228
|
+
if (status === "loading") {
|
|
229
|
+
return /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "\u266A Loading..." });
|
|
230
|
+
}
|
|
231
|
+
if (status === "playing") {
|
|
232
|
+
return /* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
|
|
233
|
+
"\u266A Playing \u2014 press ",
|
|
234
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "p" }),
|
|
235
|
+
" to pause"
|
|
236
|
+
] });
|
|
237
|
+
}
|
|
238
|
+
if (status === "paused") {
|
|
239
|
+
return /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
240
|
+
"\u266A Paused \u2014 press ",
|
|
241
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "p" }),
|
|
242
|
+
" to resume"
|
|
243
|
+
] });
|
|
244
|
+
}
|
|
245
|
+
return /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
246
|
+
"\u266A Press ",
|
|
247
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "p" }),
|
|
248
|
+
" to play music"
|
|
249
|
+
] });
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// source/components/mascot.tsx
|
|
253
|
+
import { Box as Box4, Text as Text5 } from "ink";
|
|
254
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
255
|
+
var R = "#E60012";
|
|
256
|
+
var W = "#FFFFFF";
|
|
257
|
+
var S = "#FFDAB9";
|
|
258
|
+
var K = "#000000";
|
|
259
|
+
var Mascot = () => /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", width: 11, children: [
|
|
260
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
261
|
+
" ",
|
|
262
|
+
/* @__PURE__ */ jsx5(Text5, { color: R, children: "\u2584\u2584\u2588\u2588\u2588\u2584\u2584" }),
|
|
263
|
+
" "
|
|
264
|
+
] }),
|
|
265
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
266
|
+
/* @__PURE__ */ jsx5(Text5, { color: R, children: "\u2584\u2588" }),
|
|
267
|
+
/* @__PURE__ */ jsx5(Text5, { color: R, backgroundColor: R, children: "\u2580\u2580" }),
|
|
268
|
+
/* @__PURE__ */ jsx5(Text5, { color: R, children: "\u2588\u2588\u2588" }),
|
|
269
|
+
/* @__PURE__ */ jsx5(Text5, { color: R, backgroundColor: R, children: "\u2580\u2580" }),
|
|
270
|
+
/* @__PURE__ */ jsx5(Text5, { color: R, children: "\u2588\u2584" })
|
|
271
|
+
] }),
|
|
272
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
273
|
+
" ",
|
|
274
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2588" }),
|
|
275
|
+
/* @__PURE__ */ jsx5(Text5, { color: W, backgroundColor: K, children: "\u2022" }),
|
|
276
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2588\u2588\u2588" }),
|
|
277
|
+
/* @__PURE__ */ jsx5(Text5, { color: W, backgroundColor: K, children: "\u2022" }),
|
|
278
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2588" }),
|
|
279
|
+
" "
|
|
280
|
+
] }),
|
|
281
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
282
|
+
" ",
|
|
283
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2588\u2588\u2588\u2588\u2588\u2588\u2588" }),
|
|
284
|
+
" "
|
|
285
|
+
] })
|
|
286
|
+
] });
|
|
287
|
+
|
|
288
|
+
// source/hooks/use-audio-player.ts
|
|
289
|
+
import { useState as useState2, useEffect, useRef as useRef2, useCallback as useCallback2 } from "react";
|
|
290
|
+
import { spawn } from "child_process";
|
|
291
|
+
import { createWriteStream, unlinkSync, existsSync } from "fs";
|
|
292
|
+
import { tmpdir } from "os";
|
|
293
|
+
import { join } from "path";
|
|
294
|
+
import { get as httpsGet } from "https";
|
|
295
|
+
import { get as httpGet } from "http";
|
|
296
|
+
function useAudioPlayer(url) {
|
|
297
|
+
const [status, setStatus] = useState2("idle");
|
|
298
|
+
const processRef = useRef2(null);
|
|
299
|
+
const tmpFileRef = useRef2(null);
|
|
300
|
+
const mountedRef = useRef2(true);
|
|
301
|
+
const getTmpPath = () => {
|
|
302
|
+
if (!tmpFileRef.current) {
|
|
303
|
+
tmpFileRef.current = join(tmpdir(), `fango-music-${Date.now()}.mp3`);
|
|
304
|
+
}
|
|
305
|
+
return tmpFileRef.current;
|
|
306
|
+
};
|
|
307
|
+
const download = useCallback2(
|
|
308
|
+
() => new Promise((resolve, reject) => {
|
|
309
|
+
const dest = getTmpPath();
|
|
310
|
+
if (existsSync(dest)) {
|
|
311
|
+
resolve(dest);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const getter = url.startsWith("https") ? httpsGet : httpGet;
|
|
315
|
+
const file = createWriteStream(dest);
|
|
316
|
+
const request = getter(url, (res) => {
|
|
317
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
318
|
+
file.close();
|
|
319
|
+
const redirectGet = res.headers.location.startsWith("https") ? httpsGet : httpGet;
|
|
320
|
+
redirectGet(res.headers.location, (redirectRes) => {
|
|
321
|
+
redirectRes.pipe(file);
|
|
322
|
+
file.on("finish", () => {
|
|
323
|
+
file.close();
|
|
324
|
+
resolve(dest);
|
|
325
|
+
});
|
|
326
|
+
}).on("error", reject);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
res.pipe(file);
|
|
330
|
+
file.on("finish", () => {
|
|
331
|
+
file.close();
|
|
332
|
+
resolve(dest);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
request.on("error", (err) => {
|
|
336
|
+
file.close();
|
|
337
|
+
try {
|
|
338
|
+
unlinkSync(dest);
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
reject(err);
|
|
342
|
+
});
|
|
343
|
+
}),
|
|
344
|
+
[url]
|
|
345
|
+
);
|
|
346
|
+
const getPlayCommand = () => {
|
|
347
|
+
if (process.platform === "darwin") {
|
|
348
|
+
return { cmd: "afplay", args: (f) => [f] };
|
|
349
|
+
}
|
|
350
|
+
return { cmd: "mpv", args: (f) => ["--no-video", f] };
|
|
351
|
+
};
|
|
352
|
+
const play = useCallback2(async () => {
|
|
353
|
+
if (status === "playing") return;
|
|
354
|
+
if (status === "paused" && processRef.current) {
|
|
355
|
+
processRef.current.kill("SIGCONT");
|
|
356
|
+
setStatus("playing");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
setStatus("loading");
|
|
360
|
+
try {
|
|
361
|
+
const filePath = await download();
|
|
362
|
+
if (!mountedRef.current) return;
|
|
363
|
+
const { cmd, args } = getPlayCommand();
|
|
364
|
+
const proc = spawn(cmd, args(filePath), { stdio: "ignore" });
|
|
365
|
+
processRef.current = proc;
|
|
366
|
+
setStatus("playing");
|
|
367
|
+
proc.on("close", () => {
|
|
368
|
+
if (!mountedRef.current) return;
|
|
369
|
+
processRef.current = null;
|
|
370
|
+
setStatus("idle");
|
|
371
|
+
});
|
|
372
|
+
proc.on("error", () => {
|
|
373
|
+
if (!mountedRef.current) return;
|
|
374
|
+
processRef.current = null;
|
|
375
|
+
setStatus("idle");
|
|
376
|
+
});
|
|
377
|
+
} catch {
|
|
378
|
+
if (mountedRef.current) setStatus("idle");
|
|
379
|
+
}
|
|
380
|
+
}, [status, download]);
|
|
381
|
+
const pause = useCallback2(() => {
|
|
382
|
+
if (status === "playing" && processRef.current) {
|
|
383
|
+
processRef.current.kill("SIGSTOP");
|
|
384
|
+
setStatus("paused");
|
|
385
|
+
}
|
|
386
|
+
}, [status]);
|
|
387
|
+
const stop = useCallback2(() => {
|
|
388
|
+
if (processRef.current) {
|
|
389
|
+
processRef.current.kill("SIGKILL");
|
|
390
|
+
processRef.current = null;
|
|
391
|
+
}
|
|
392
|
+
setStatus("idle");
|
|
393
|
+
}, []);
|
|
394
|
+
const toggle = useCallback2(() => {
|
|
395
|
+
if (status === "playing") {
|
|
396
|
+
pause();
|
|
397
|
+
} else {
|
|
398
|
+
void play();
|
|
399
|
+
}
|
|
400
|
+
}, [status, pause, play]);
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
mountedRef.current = true;
|
|
403
|
+
return () => {
|
|
404
|
+
mountedRef.current = false;
|
|
405
|
+
if (processRef.current) {
|
|
406
|
+
processRef.current.kill("SIGKILL");
|
|
407
|
+
processRef.current = null;
|
|
408
|
+
}
|
|
409
|
+
if (tmpFileRef.current && existsSync(tmpFileRef.current)) {
|
|
410
|
+
try {
|
|
411
|
+
unlinkSync(tmpFileRef.current);
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}, []);
|
|
417
|
+
return { status, toggle, play, pause, stop };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// source/app.tsx
|
|
421
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
183
422
|
var App = () => {
|
|
184
423
|
const { exit } = useApp();
|
|
424
|
+
const audio = useAudioPlayer(musicUrl);
|
|
185
425
|
useInput2((input) => {
|
|
186
426
|
if (input === "q") {
|
|
427
|
+
audio.stop();
|
|
187
428
|
exit();
|
|
188
429
|
}
|
|
430
|
+
if (input === "p") {
|
|
431
|
+
audio.toggle();
|
|
432
|
+
}
|
|
189
433
|
});
|
|
190
|
-
return /* @__PURE__ */
|
|
191
|
-
|
|
434
|
+
return /* @__PURE__ */ jsxs6(
|
|
435
|
+
Box5,
|
|
192
436
|
{
|
|
193
437
|
flexDirection: "column",
|
|
194
438
|
borderStyle: "round",
|
|
@@ -196,28 +440,33 @@ var App = () => {
|
|
|
196
440
|
paddingY: 1,
|
|
197
441
|
width: 54,
|
|
198
442
|
children: [
|
|
199
|
-
/* @__PURE__ */
|
|
200
|
-
/* @__PURE__ */
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
443
|
+
/* @__PURE__ */ jsxs6(Box5, { children: [
|
|
444
|
+
/* @__PURE__ */ jsx6(Box5, { width: 12, flexShrink: 0, children: /* @__PURE__ */ jsx6(Mascot, {}) }),
|
|
445
|
+
/* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", flexGrow: 1, children: [
|
|
446
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
447
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "white", children: [
|
|
448
|
+
profile.fullName.first,
|
|
449
|
+
" "
|
|
450
|
+
] }),
|
|
451
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: profile.fullName.last }),
|
|
452
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
453
|
+
" (",
|
|
454
|
+
profile.name,
|
|
455
|
+
")"
|
|
456
|
+
] })
|
|
457
|
+
] }),
|
|
458
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", children: profile.title }),
|
|
459
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: profile.tagline })
|
|
210
460
|
] })
|
|
211
461
|
] }),
|
|
212
|
-
/* @__PURE__ */
|
|
213
|
-
/* @__PURE__ */
|
|
214
|
-
/* @__PURE__ */
|
|
215
|
-
/* @__PURE__ */
|
|
216
|
-
|
|
217
|
-
/* @__PURE__ */
|
|
218
|
-
|
|
219
|
-
/* @__PURE__ */
|
|
220
|
-
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
462
|
+
/* @__PURE__ */ jsx6(MusicPlayer, { status: audio.status }),
|
|
463
|
+
/* @__PURE__ */ jsx6(Experience, { experiences }),
|
|
464
|
+
/* @__PURE__ */ jsx6(Projects, { projects }),
|
|
465
|
+
/* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", paddingTop: 1, children: [
|
|
466
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: "Education" }),
|
|
467
|
+
/* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", children: [
|
|
468
|
+
/* @__PURE__ */ jsx6(Text6, { children: education.school }),
|
|
469
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
221
470
|
education.degree,
|
|
222
471
|
" ",
|
|
223
472
|
education.from,
|
|
@@ -226,12 +475,12 @@ var App = () => {
|
|
|
226
475
|
] })
|
|
227
476
|
] })
|
|
228
477
|
] }),
|
|
229
|
-
/* @__PURE__ */
|
|
478
|
+
/* @__PURE__ */ jsx6(Links, { links })
|
|
230
479
|
]
|
|
231
480
|
}
|
|
232
481
|
);
|
|
233
482
|
};
|
|
234
483
|
|
|
235
484
|
// source/cli.tsx
|
|
236
|
-
import { jsx as
|
|
237
|
-
render(/* @__PURE__ */
|
|
485
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
486
|
+
render(/* @__PURE__ */ jsx7(App, {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fangge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Fango's terminal business card — run npx fango",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"react": "^18.3.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.6.0",
|
|
35
36
|
"@types/react": "^18.3.28",
|
|
36
37
|
"tsup": "^8.5.1",
|
|
37
38
|
"tsx": "^4.21.0",
|