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.
Files changed (2) hide show
  1. package/dist/cli.js +282 -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://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
- { 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,216 @@ 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 = "#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__ */ jsxs4(
191
- Box4,
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__ */ 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
- ")"
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__ */ 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: [
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__ */ jsx4(Links, { links })
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 jsx5 } from "react/jsx-runtime";
237
- render(/* @__PURE__ */ jsx5(App, {}));
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.2",
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",