yechancho 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/index.js +229 -67
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -9,73 +9,122 @@ import terminalLink from 'terminal-link'
9
9
  import logUpdate from 'log-update'
10
10
  import ansiEscapes from 'ansi-escapes'
11
11
  import cliCursor from 'cli-cursor'
12
+ import readline from 'node:readline'
13
+
14
+ /**
15
+ * Flags:
16
+ * --static no animation, show card only
17
+ * --duration=2500 opening animation length in ms (default 2.5s, then overlay clears)
18
+ * --fps=30 animation fps (default 30)
19
+ */
20
+
21
+ const args = new Map(
22
+ process.argv.slice(2).map((a) => {
23
+ const [k, v] = a.split('=')
24
+ return [k, v ?? true]
25
+ })
26
+ )
12
27
 
13
28
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
14
29
 
30
+ // Sakura palette
15
31
  const sakura = {
32
+ soft: chalk.hex('#FFD7E5'),
16
33
  pink: chalk.hex('#FFB7C5'),
17
- blossom: chalk.hex('#FFD7E5'),
18
34
  rose: chalk.hex('#FF6FAE'),
19
35
  lilac: chalk.hex('#C7A2FF'),
20
36
  sky: chalk.hex('#7FD1FF'),
21
37
  mint: chalk.hex('#8CFFDB'),
22
38
  dim: chalk.hex('#9AA0A6'),
39
+ white: chalk.hex('#F8F8FF'),
23
40
  }
24
41
 
25
- const links = [
26
- {
27
- label: 'LinkedIn',
28
- url: 'https://www.linkedin.com/in/yechancho5',
29
- icon: '',
30
- },
31
- {
32
- label: 'Github',
33
- url: 'https://www.github.com/in/yechancho5',
34
- icon: '❀',
35
- },
42
+ const supportsLinks = terminalLink.isSupported
43
+
44
+ const LINKS = [
45
+ { label: 'LinkedIn', url: 'https://www.linkedin.com/in/yechancho5', icon: '🌸' },
46
+ { label: 'GitHub', url: 'https://github.com/yechancho5', icon: '' },
36
47
  ]
37
48
 
38
- const supportsLinks = terminalLink.isSupported
49
+ const TECH = ['React', 'TypeScript', 'FastAPI', 'Postgres', 'AWS', 'RAG']
50
+ const NOW = sakura.soft('building: ') + sakura.mint('cool products + shipping fast')
51
+
52
+ function clamp(n, a, b) {
53
+ return Math.max(a, Math.min(b, n))
54
+ }
39
55
 
40
- const makeHeader = () => {
41
- const text = figlet.textSync('YECHAN', { font: 'ANSI Shadow', horizontalLayout: 'default' })
56
+ function makeHeader() {
57
+ const text = figlet.textSync('YECHAN', {
58
+ font: 'ANSI Shadow',
59
+ horizontalLayout: 'default',
60
+ })
42
61
  return gradient(['#FFB7C5', '#FFD7E5', '#C7A2FF'])(text)
43
62
  }
44
63
 
45
- const makeBody = () => {
46
- const bio =
47
- sakura.blossom("hi, i'm yechan ") +
48
- sakura.dim('— ') +
49
- sakura.pink('i build things ') +
50
- sakura.dim(' ') +
51
- sakura.lilac('full-stack') +
52
- sakura.dim(' • ') +
53
- sakura.sky('systems') +
54
- '\n'
55
-
56
- const line = sakura.dim('─'.repeat(36))
57
-
58
- const linkLines = links
59
- .map(({ label, url, icon }) => {
60
- const left = sakura.rose(` ${icon} `) + sakura.pink(`${label}:`).padEnd(12)
61
- const right = supportsLinks ? terminalLink(sakura.sky(url), url) : sakura.sky(url)
62
- return left + ' ' + right
63
- })
64
- .join('\n')
64
+ function makeBranchArt() {
65
+ const cols = process.stdout.columns ?? 80
66
+ if (cols < 70) return ''
67
+
68
+ const art = [
69
+ " .::' '::..",
70
+ " .:' `:.",
71
+ " :: _.._ ::",
72
+ " `:..-' `-..:'",
73
+ " \\ 🌸 /",
74
+ " _.-'\\ /`-._",
75
+ " .' \\__/ `.",
76
+ " / ✿ .-..-. ❀ \\",
77
+ " \\ ( ) /",
78
+ " `-._ `-..-' _.-'",
79
+ " `--. .--'",
80
+ " \\/",
81
+ ].join('\n')
82
+
83
+ return gradient(['#FFD7E5', '#FFB7C5', '#C7A2FF'])(art)
84
+ }
85
+
86
+ function sectionTitle(t) {
87
+ return sakura.rose('✿ ') + sakura.white.bold(t)
88
+ }
89
+
90
+ function pill(text) {
91
+ return chalk.hex('#1A1A1A').bgHex('#FFD7E5')(` ${text} `)
92
+ }
93
+
94
+ function makeBody() {
95
+ const about =
96
+ sakura.white("hi, i'm ") +
97
+ sakura.pink.bold('yechan') +
98
+ sakura.dim(' — ') +
99
+ sakura.soft('i like building useful things & hanging with friends.')
65
100
 
66
- const footer =
67
- '\n' +
68
- sakura.dim('say hi: ') +
69
- sakura.mint('yechancho') +
70
- sakura.dim(' @ npm') +
71
- ' ' +
72
- sakura.rose('🌸')
101
+ const linkLines = LINKS.map(({ label, url, icon }) => {
102
+ const left = sakura.pink(`${icon} ${label}:`).padEnd(14)
103
+ const right = supportsLinks ? terminalLink(sakura.sky(url), url) : sakura.sky(url)
104
+ return left + right
105
+ }).join('\n')
73
106
 
74
- return [bio, line, linkLines, footer].join('\n')
107
+ const techLine = TECH.map((t) => pill(t)).join(' ')
108
+
109
+ return [
110
+ about,
111
+ '',
112
+ sectionTitle('links'),
113
+ linkLines,
114
+ '',
115
+ sectionTitle('stack'),
116
+ techLine,
117
+ '',
118
+ sectionTitle('now'),
119
+ NOW,
120
+ '',
121
+ sakura.dim('run: ') + sakura.pink('npx @yechancho/terminal-card') + ' ' + sakura.rose('🌸'),
122
+ ].join('\n')
75
123
  }
76
124
 
77
- const makeCard = () => {
78
- const content = `${makeHeader()}\n\n${makeBody()}`
125
+ function makeCard() {
126
+ const branch = makeBranchArt()
127
+ const content = `${makeHeader()}\n${branch ? branch + '\n' : ''}\n${makeBody()}`
79
128
  return boxen(content, {
80
129
  padding: 1,
81
130
  margin: 1,
@@ -84,49 +133,162 @@ const makeCard = () => {
84
133
  })
85
134
  }
86
135
 
87
- function renderPetalsFrame(card, frame, width = 60, height = 12) {
88
- const petals = ['🌸', '✿', '❀', '·']
89
- const cols = width
90
- const rows = height
91
-
92
- const points = []
93
- for (let i = 0; i < 18; i++) {
94
- const x = (i * 7 + frame * 3) % cols
95
- const y = (i * 3 + frame) % rows
96
- const ch = petals[(i + frame) % petals.length]
97
- points.push({ x, y, ch })
136
+ const PETALS = ['🌸', '❀', '✿', '·', '•']
137
+
138
+ function rand(seed) {
139
+ // tiny deterministic-ish PRNG
140
+ const x = Math.sin(seed) * 10000
141
+ return x - Math.floor(x)
142
+ }
143
+
144
+ function initPetals(count, width, height) {
145
+ const petals = []
146
+ for (let i = 0; i < count; i++) {
147
+ const r1 = rand(i * 13.37)
148
+ const r2 = rand(i * 42.42)
149
+ const r3 = rand(i * 7.77)
150
+ petals.push({
151
+ x: Math.floor(r1 * width),
152
+ y: Math.floor(r2 * height),
153
+ vy: 0.25 + r3 * 0.85, // fall speed
154
+ drift: (r2 - 0.5) * 0.6, // horizontal drift
155
+ sway: 0.6 + r1 * 1.2, // sway frequency
156
+ ch: PETALS[i % PETALS.length],
157
+ })
98
158
  }
159
+ return petals
160
+ }
161
+
162
+ function renderOverlay(petals, t, width, height) {
163
+ const grid = Array.from({ length: height }, () => Array.from({ length: width }, () => ' '))
99
164
 
100
- const grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ' '))
101
- for (const p of points) grid[p.y][p.x] = p.ch
165
+ for (const p of petals) {
166
+ const sway = Math.sin((t / 220) * p.sway) * 1.5
167
+ const x = Math.round(p.x + sway)
168
+ const y = Math.round(p.y)
169
+
170
+ if (y >= 0 && y < height && x >= 0 && x < width) {
171
+ grid[y][x] = p.ch
172
+ }
173
+ }
102
174
 
103
175
  const overlay = grid.map((r) => r.join('')).join('\n')
104
- const tinted = sakura.blossom(overlay)
176
+ return sakura.soft(overlay)
177
+ }
105
178
 
106
- return tinted + '\n' + card
179
+ function stepPetals(petals, width, height) {
180
+ for (const p of petals) {
181
+ p.y += p.vy
182
+ p.x += p.drift
183
+
184
+ // wrap around
185
+ if (p.y >= height) {
186
+ p.y = 0
187
+ p.x = Math.floor(Math.random() * width)
188
+ }
189
+ if (p.x < 0) p.x = width - 1
190
+ if (p.x >= width) p.x = 0
191
+ }
192
+ }
193
+
194
+
195
+ function setupExitCleanup() {
196
+ const cleanup = () => {
197
+ try {
198
+ cliCursor.show()
199
+ logUpdate.clear()
200
+ process.stdout.write(ansiEscapes.cursorShow)
201
+ } catch {}
202
+ }
203
+
204
+ process.on('SIGINT', () => {
205
+ cleanup()
206
+ process.exit(0)
207
+ })
208
+ process.on('SIGTERM', () => {
209
+ cleanup()
210
+ process.exit(0)
211
+ })
212
+ process.on('exit', cleanup)
213
+
214
+ return cleanup
215
+ }
216
+
217
+ async function waitForKeyOrDuration(durationMs) {
218
+ return new Promise((resolve) => {
219
+ const done = () => resolve()
220
+
221
+ let timer = null
222
+ if (Number.isFinite(durationMs) && durationMs > 0) {
223
+ timer = setTimeout(done, durationMs)
224
+ }
225
+
226
+ readline.emitKeypressEvents(process.stdin)
227
+ if (process.stdin.isTTY) process.stdin.setRawMode(true)
228
+ process.stdin.resume()
229
+
230
+ const onKey = () => {
231
+ if (timer) clearTimeout(timer)
232
+ process.stdin.off('keypress', onKey)
233
+ done()
234
+ }
235
+
236
+ process.stdin.on('keypress', onKey)
237
+ })
107
238
  }
108
239
 
109
240
  async function main() {
110
241
  const card = makeCard()
111
242
 
112
- if (process.argv.includes('--static')) {
243
+ if (args.get('--static')) {
113
244
  console.log(card)
114
245
  return
115
246
  }
116
247
 
248
+ const fps = clamp(parseInt(args.get('--fps') ?? '30', 10) || 30, 10, 60)
249
+ const frameMs = Math.round(1000 / fps)
250
+
251
+ const cleanup = setupExitCleanup()
252
+
117
253
  cliCursor.hide()
118
254
  process.stdout.write(ansiEscapes.clearScreen)
119
255
 
120
- const frames = 28
121
- for (let f = 0; f < frames; f++) {
122
- logUpdate(renderPetalsFrame(card, f))
123
- await sleep(45)
256
+ // Overlay size adapts to terminal
257
+ const termW = process.stdout.columns ?? 80
258
+ const termH = process.stdout.rows ?? 24
259
+
260
+ const overlayW = clamp(termW - 2, 40, 120)
261
+ const overlayH = clamp(Math.floor(termH / 2), 8, 18)
262
+
263
+ // More petals on bigger screens
264
+ const petalCount = clamp(Math.floor(overlayW / 4), 12, 40)
265
+ const petals = initPetals(petalCount, overlayW, overlayH)
266
+
267
+ const durationArg = args.get('--duration')
268
+ const durationMs = durationArg === true ? 2500 : parseInt(durationArg ?? '2500', 10) || 2500
269
+
270
+ let running = true
271
+ const stopPromise = waitForKeyOrDuration(durationMs).then(() => (running = false))
272
+
273
+ let t = 0
274
+ while (running) {
275
+ const overlay = renderOverlay(petals, t, overlayW, overlayH)
276
+ const sparkleLine =
277
+ sakura.rose('✿') +
278
+ sakura.dim(' '.repeat(Math.max(0, overlayW - 4))) +
279
+ sakura.rose('✿')
280
+
281
+ logUpdate(`${overlay}\n${sparkleLine}\n${card}`)
282
+ stepPetals(petals, overlayW, overlayH)
283
+ t += frameMs
284
+ await sleep(frameMs)
124
285
  }
125
286
 
287
+ await stopPromise
288
+
126
289
  logUpdate.clear()
127
- cliCursor.show()
290
+ cleanup()
128
291
  console.log(card)
129
- console.log(sakura.dim('Tip: run with ') + sakura.pink('--static') + sakura.dim(' to disable animation.'))
130
292
  }
131
293
 
132
294
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yechancho",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "yechancho's terminal card",
5
5
  "main": "index.js",
6
6
  "type": "module",