fangge 1.0.2 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +284 -35
  2. 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 Box4, Text as Text4, useApp, useInput as useInput2 } from "ink";
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
- { name: "ListenHub", description: "AI audio platform", href: "https://listenhub.ai" },
48
- { name: "LH Skills", description: "AI skills for Claude Code", href: "https://github.com/marswaveai/skills" },
49
- { name: "LH SDK", description: "TypeScript SDK", href: "https://github.com/marswaveai/listenhub-sdk" },
50
- { name: "fango-cli", description: "This terminal card", href: "https://github.com/0xFANGO/fango-cli" }
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
- { label: "LH Skills", href: "https://github.com/marswaveai/skills", display: "github.com/marswaveai/skills" },
61
- { label: "LH SDK", href: "https://github.com/marswaveai/listenhub-sdk", display: "github.com/marswaveai/listenhub-sdk" },
62
- { label: "GitHub", href: "https://github.com/0xFANGO", display: "github.com/0xFANGO" },
63
- { label: "Email", href: "mailto:silencerichard@163.com", display: "silencerichard@163.com" },
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/app.tsx
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__ */ jsxs4(
191
- Box4,
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__ */ jsxs4(Text4, { children: [
200
- /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "white", children: [
201
- "\u266A ",
202
- profile.fullName.first,
203
- " "
204
- ] }),
205
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "red", children: profile.fullName.last }),
206
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
207
- " (",
208
- profile.name,
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__ */ jsx4(Text4, { color: "cyan", children: profile.title }),
213
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: profile.tagline }),
214
- /* @__PURE__ */ jsx4(Experience, { experiences }),
215
- /* @__PURE__ */ jsx4(Projects, { projects }),
216
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingTop: 1, children: [
217
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "red", children: "Education" }),
218
- /* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
219
- /* @__PURE__ */ jsx4(Text4, { children: education.school }),
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__ */ jsx4(Links, { links })
478
+ /* @__PURE__ */ jsx6(Links, { links })
230
479
  ]
231
480
  }
232
481
  );
233
482
  };
234
483
 
235
484
  // source/cli.tsx
236
- import { jsx as jsx5 } from "react/jsx-runtime";
237
- render(/* @__PURE__ */ jsx5(App, {}));
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.2",
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",