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.
- package/index.js +231 -67
- 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
|
|
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/yechancho', icon: '⌘' },
|
|
47
|
+
// { label: 'Website', url: 'https://yechancho.com', icon: '✿' },
|
|
36
48
|
]
|
|
37
49
|
|
|
38
|
-
const
|
|
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
|
-
|
|
41
|
-
const text = figlet.textSync('YECHAN', {
|
|
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
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
sakura.
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
const
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
177
|
+
return sakura.soft(overlay)
|
|
178
|
+
}
|
|
105
179
|
|
|
106
|
-
|
|
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 (
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
291
|
+
cleanup()
|
|
128
292
|
console.log(card)
|
|
129
|
-
console.log(sakura.dim('
|
|
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) => {
|