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.
- package/index.js +229 -67
- 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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
41
|
-
const text = figlet.textSync('YECHAN', {
|
|
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
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
sakura.
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
const
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
176
|
+
return sakura.soft(overlay)
|
|
177
|
+
}
|
|
105
178
|
|
|
106
|
-
|
|
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 (
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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) => {
|