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