fangge 1.0.2 → 1.0.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/dist/cli.js +282 -35
- package/package.json +2 -1
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://archive.org/download/f7db6af68ab12dfa9894e8f6fff5e370954f6395457a5c5c00b45874a49138ec9a4b3f145fcc5e88/Super%20Mario%20Bros%20Owld.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,216 @@ 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 = "#CE2029";
|
|
256
|
+
var H = "#6B420C";
|
|
257
|
+
var S = "#E8A860";
|
|
258
|
+
var Mascot = () => /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", width: 12, children: [
|
|
259
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
260
|
+
" ",
|
|
261
|
+
/* @__PURE__ */ jsx5(Text5, { color: R, children: "\u2584\u2588\u2588\u2588\u2588\u2588\u2584\u2584\u2584" }),
|
|
262
|
+
" "
|
|
263
|
+
] }),
|
|
264
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
265
|
+
/* @__PURE__ */ jsx5(Text5, { color: H, children: "\u2584" }),
|
|
266
|
+
/* @__PURE__ */ jsx5(Text5, { color: H, backgroundColor: S, children: "\u2580" }),
|
|
267
|
+
/* @__PURE__ */ jsx5(Text5, { color: H, children: "\u2588" }),
|
|
268
|
+
/* @__PURE__ */ jsx5(Text5, { color: H, backgroundColor: S, children: "\u2580" }),
|
|
269
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2588\u2588" }),
|
|
270
|
+
/* @__PURE__ */ jsx5(Text5, { color: H, children: "\u2588" }),
|
|
271
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2588\u2584\u2584" }),
|
|
272
|
+
" "
|
|
273
|
+
] }),
|
|
274
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
275
|
+
/* @__PURE__ */ jsx5(Text5, { color: H, children: "\u2588" }),
|
|
276
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, backgroundColor: H, children: "\u2580" }),
|
|
277
|
+
/* @__PURE__ */ jsx5(Text5, { color: H, backgroundColor: S, children: "\u2580\u2580" }),
|
|
278
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2588\u2588" }),
|
|
279
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, backgroundColor: H, children: "\u2580" }),
|
|
280
|
+
/* @__PURE__ */ jsx5(Text5, { color: H, children: "\u2588" }),
|
|
281
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, backgroundColor: H, children: "\u2580\u2580" }),
|
|
282
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2580" })
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
285
|
+
" ",
|
|
286
|
+
/* @__PURE__ */ jsx5(Text5, { color: S, children: "\u2580\u2580\u2580\u2580\u2580\u2580\u2580" }),
|
|
287
|
+
" "
|
|
288
|
+
] })
|
|
289
|
+
] });
|
|
290
|
+
|
|
291
|
+
// source/hooks/use-audio-player.ts
|
|
292
|
+
import { useState as useState2, useEffect, useRef as useRef2, useCallback as useCallback2 } from "react";
|
|
293
|
+
import { spawn } from "child_process";
|
|
294
|
+
import { createWriteStream, unlinkSync, existsSync } from "fs";
|
|
295
|
+
import { tmpdir } from "os";
|
|
296
|
+
import { join } from "path";
|
|
297
|
+
import { get as httpsGet } from "https";
|
|
298
|
+
import { get as httpGet } from "http";
|
|
299
|
+
function useAudioPlayer(url) {
|
|
300
|
+
const [status, setStatus] = useState2("idle");
|
|
301
|
+
const processRef = useRef2(null);
|
|
302
|
+
const tmpFileRef = useRef2(null);
|
|
303
|
+
const mountedRef = useRef2(true);
|
|
304
|
+
const getTmpPath = () => {
|
|
305
|
+
if (!tmpFileRef.current) {
|
|
306
|
+
tmpFileRef.current = join(tmpdir(), `fango-music-${Date.now()}.mp3`);
|
|
307
|
+
}
|
|
308
|
+
return tmpFileRef.current;
|
|
309
|
+
};
|
|
310
|
+
const download = useCallback2(
|
|
311
|
+
() => new Promise((resolve, reject) => {
|
|
312
|
+
const dest = getTmpPath();
|
|
313
|
+
if (existsSync(dest)) {
|
|
314
|
+
resolve(dest);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const file = createWriteStream(dest);
|
|
318
|
+
const follow = (target, maxRedirects = 5) => {
|
|
319
|
+
const getter = target.startsWith("https") ? httpsGet : httpGet;
|
|
320
|
+
getter(target, (res) => {
|
|
321
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && maxRedirects > 0) {
|
|
322
|
+
res.resume();
|
|
323
|
+
follow(res.headers.location, maxRedirects - 1);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
res.pipe(file);
|
|
327
|
+
file.on("finish", () => {
|
|
328
|
+
file.close();
|
|
329
|
+
resolve(dest);
|
|
330
|
+
});
|
|
331
|
+
}).on("error", (err) => {
|
|
332
|
+
file.close();
|
|
333
|
+
try {
|
|
334
|
+
unlinkSync(dest);
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
reject(err);
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
follow(url);
|
|
341
|
+
}),
|
|
342
|
+
[url]
|
|
343
|
+
);
|
|
344
|
+
const getPlayCommand = () => {
|
|
345
|
+
if (process.platform === "darwin") {
|
|
346
|
+
return { cmd: "afplay", args: (f) => [f] };
|
|
347
|
+
}
|
|
348
|
+
return { cmd: "mpv", args: (f) => ["--no-video", f] };
|
|
349
|
+
};
|
|
350
|
+
const play = useCallback2(async () => {
|
|
351
|
+
if (status === "playing") return;
|
|
352
|
+
if (status === "paused" && processRef.current) {
|
|
353
|
+
processRef.current.kill("SIGCONT");
|
|
354
|
+
setStatus("playing");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
setStatus("loading");
|
|
358
|
+
try {
|
|
359
|
+
const filePath = await download();
|
|
360
|
+
if (!mountedRef.current) return;
|
|
361
|
+
const { cmd, args } = getPlayCommand();
|
|
362
|
+
const proc = spawn(cmd, args(filePath), { stdio: "ignore" });
|
|
363
|
+
processRef.current = proc;
|
|
364
|
+
setStatus("playing");
|
|
365
|
+
proc.on("close", () => {
|
|
366
|
+
if (!mountedRef.current) return;
|
|
367
|
+
processRef.current = null;
|
|
368
|
+
setStatus("idle");
|
|
369
|
+
});
|
|
370
|
+
proc.on("error", () => {
|
|
371
|
+
if (!mountedRef.current) return;
|
|
372
|
+
processRef.current = null;
|
|
373
|
+
setStatus("idle");
|
|
374
|
+
});
|
|
375
|
+
} catch {
|
|
376
|
+
if (mountedRef.current) setStatus("idle");
|
|
377
|
+
}
|
|
378
|
+
}, [status, download]);
|
|
379
|
+
const pause = useCallback2(() => {
|
|
380
|
+
if (status === "playing" && processRef.current) {
|
|
381
|
+
processRef.current.kill("SIGSTOP");
|
|
382
|
+
setStatus("paused");
|
|
383
|
+
}
|
|
384
|
+
}, [status]);
|
|
385
|
+
const stop = useCallback2(() => {
|
|
386
|
+
if (processRef.current) {
|
|
387
|
+
processRef.current.kill("SIGKILL");
|
|
388
|
+
processRef.current = null;
|
|
389
|
+
}
|
|
390
|
+
setStatus("idle");
|
|
391
|
+
}, []);
|
|
392
|
+
const toggle = useCallback2(() => {
|
|
393
|
+
if (status === "playing") {
|
|
394
|
+
pause();
|
|
395
|
+
} else {
|
|
396
|
+
void play();
|
|
397
|
+
}
|
|
398
|
+
}, [status, pause, play]);
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
mountedRef.current = true;
|
|
401
|
+
return () => {
|
|
402
|
+
mountedRef.current = false;
|
|
403
|
+
if (processRef.current) {
|
|
404
|
+
processRef.current.kill("SIGKILL");
|
|
405
|
+
processRef.current = null;
|
|
406
|
+
}
|
|
407
|
+
if (tmpFileRef.current && existsSync(tmpFileRef.current)) {
|
|
408
|
+
try {
|
|
409
|
+
unlinkSync(tmpFileRef.current);
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}, []);
|
|
415
|
+
return { status, toggle, play, pause, stop };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// source/app.tsx
|
|
419
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
183
420
|
var App = () => {
|
|
184
421
|
const { exit } = useApp();
|
|
422
|
+
const audio = useAudioPlayer(musicUrl);
|
|
185
423
|
useInput2((input) => {
|
|
186
424
|
if (input === "q") {
|
|
425
|
+
audio.stop();
|
|
187
426
|
exit();
|
|
188
427
|
}
|
|
428
|
+
if (input === "p") {
|
|
429
|
+
audio.toggle();
|
|
430
|
+
}
|
|
189
431
|
});
|
|
190
|
-
return /* @__PURE__ */
|
|
191
|
-
|
|
432
|
+
return /* @__PURE__ */ jsxs6(
|
|
433
|
+
Box5,
|
|
192
434
|
{
|
|
193
435
|
flexDirection: "column",
|
|
194
436
|
borderStyle: "round",
|
|
@@ -196,28 +438,32 @@ var App = () => {
|
|
|
196
438
|
paddingY: 1,
|
|
197
439
|
width: 54,
|
|
198
440
|
children: [
|
|
199
|
-
/* @__PURE__ */
|
|
200
|
-
/* @__PURE__ */
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
441
|
+
/* @__PURE__ */ jsxs6(Box5, { children: [
|
|
442
|
+
/* @__PURE__ */ jsx6(Box5, { width: 12, flexShrink: 0, children: /* @__PURE__ */ jsx6(Mascot, {}) }),
|
|
443
|
+
/* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", flexGrow: 1, children: [
|
|
444
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
445
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "white", children: [
|
|
446
|
+
profile.fullName.first,
|
|
447
|
+
" "
|
|
448
|
+
] }),
|
|
449
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: profile.fullName.last }),
|
|
450
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
451
|
+
" (",
|
|
452
|
+
profile.name,
|
|
453
|
+
")"
|
|
454
|
+
] })
|
|
455
|
+
] }),
|
|
456
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", children: profile.title }),
|
|
457
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: profile.tagline })
|
|
210
458
|
] })
|
|
211
459
|
] }),
|
|
212
|
-
/* @__PURE__ */
|
|
213
|
-
/* @__PURE__ */
|
|
214
|
-
/* @__PURE__ */
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
/* @__PURE__ */ jsx4(Text4, { children: education.school }),
|
|
220
|
-
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
460
|
+
/* @__PURE__ */ jsx6(Experience, { experiences }),
|
|
461
|
+
/* @__PURE__ */ jsx6(Projects, { projects }),
|
|
462
|
+
/* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", paddingTop: 1, children: [
|
|
463
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "red", children: "Education" }),
|
|
464
|
+
/* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", children: [
|
|
465
|
+
/* @__PURE__ */ jsx6(Text6, { children: education.school }),
|
|
466
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
221
467
|
education.degree,
|
|
222
468
|
" ",
|
|
223
469
|
education.from,
|
|
@@ -226,12 +472,13 @@ var App = () => {
|
|
|
226
472
|
] })
|
|
227
473
|
] })
|
|
228
474
|
] }),
|
|
229
|
-
/* @__PURE__ */
|
|
475
|
+
/* @__PURE__ */ jsx6(Links, { links }),
|
|
476
|
+
/* @__PURE__ */ jsx6(MusicPlayer, { status: audio.status })
|
|
230
477
|
]
|
|
231
478
|
}
|
|
232
479
|
);
|
|
233
480
|
};
|
|
234
481
|
|
|
235
482
|
// source/cli.tsx
|
|
236
|
-
import { jsx as
|
|
237
|
-
render(/* @__PURE__ */
|
|
483
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
484
|
+
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.4",
|
|
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",
|