@meebs/meeb 1.0.0
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/README.md +158 -0
- package/animals/alien.js +34 -0
- package/animals/axolotl.js +38 -0
- package/animals/bat.js +34 -0
- package/animals/bear.js +41 -0
- package/animals/bee.js +32 -0
- package/animals/bunny.js +45 -0
- package/animals/butterfly.js +36 -0
- package/animals/capybara.js +36 -0
- package/animals/cat.js +44 -0
- package/animals/chameleon.js +44 -0
- package/animals/chick.js +27 -0
- package/animals/cookie.js +30 -0
- package/animals/crab.js +32 -0
- package/animals/crocodile.js +32 -0
- package/animals/deer.js +35 -0
- package/animals/dino.js +37 -0
- package/animals/dog.js +44 -0
- package/animals/dragon.js +44 -0
- package/animals/duck.js +45 -0
- package/animals/eagle.js +37 -0
- package/animals/elephant.js +33 -0
- package/animals/firefly.js +37 -0
- package/animals/fish.js +32 -0
- package/animals/flamingo.js +36 -0
- package/animals/fox.js +43 -0
- package/animals/frog.js +46 -0
- package/animals/ghost.js +32 -0
- package/animals/giraffe.js +37 -0
- package/animals/gorilla.js +37 -0
- package/animals/hamster.js +35 -0
- package/animals/hedgehog.js +32 -0
- package/animals/hippo.js +34 -0
- package/animals/index.js +87 -0
- package/animals/jellyfish.js +34 -0
- package/animals/koala.js +34 -0
- package/animals/ladybug.js +34 -0
- package/animals/lion.js +36 -0
- package/animals/mantisshrimp.js +49 -0
- package/animals/monkey.js +35 -0
- package/animals/moth.js +41 -0
- package/animals/mouse.js +33 -0
- package/animals/narwhal.js +38 -0
- package/animals/octopus.js +31 -0
- package/animals/otter.js +43 -0
- package/animals/owl.js +38 -0
- package/animals/panda.js +42 -0
- package/animals/parrot.js +39 -0
- package/animals/peacock.js +42 -0
- package/animals/penguin.js +47 -0
- package/animals/penguin2.js +38 -0
- package/animals/pig.js +33 -0
- package/animals/raccoon.js +35 -0
- package/animals/redpanda.js +39 -0
- package/animals/robot.js +37 -0
- package/animals/scorpion.js +33 -0
- package/animals/seahorse.js +39 -0
- package/animals/shark.js +33 -0
- package/animals/shit.js +33 -0
- package/animals/sloth.js +34 -0
- package/animals/snail.js +34 -0
- package/animals/snake.js +36 -0
- package/animals/spider.js +33 -0
- package/animals/squid.js +33 -0
- package/animals/starfish.js +32 -0
- package/animals/stingray.js +41 -0
- package/animals/swan.js +34 -0
- package/animals/toucan.js +41 -0
- package/animals/turtle.js +35 -0
- package/animals/unicorn.js +40 -0
- package/animals/walrus.js +35 -0
- package/animals/whale.js +42 -0
- package/animals/wolf.js +36 -0
- package/animals/yak.js +36 -0
- package/animals/zebra.js +34 -0
- package/color.js +9 -0
- package/completions/_meeb +41 -0
- package/completions/meeb.bash +16 -0
- package/emoji.js +30 -0
- package/fortune.js +41 -0
- package/gif.js +435 -0
- package/index.js +915 -0
- package/info.js +80 -0
- package/package.json +39 -0
- package/png.js +88 -0
- package/svg.js +43 -0
- package/theme.js +80 -0
- package/tui.js +76 -0
- package/util.js +313 -0
package/index.js
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const animals = require('./animals')
|
|
4
|
+
const visible = Object.fromEntries(Object.entries(animals).filter(([, v]) => !v.hidden))
|
|
5
|
+
const { rgb } = require('./color')
|
|
6
|
+
const { addSay, addHat, addLabel, stripAnsi, flipArt, stretchArt, squishArt, stretchVArt, squishVArt, sideBySide } = require('./util')
|
|
7
|
+
const { resolve: emojiResolve } = require('./emoji')
|
|
8
|
+
const { getFortune } = require('./fortune')
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2)
|
|
11
|
+
const flags = {}
|
|
12
|
+
const positional = []
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
if (args[i] === '--say') {
|
|
16
|
+
const words = []
|
|
17
|
+
while (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
18
|
+
words.push(args[++i])
|
|
19
|
+
}
|
|
20
|
+
if (words.length) flags.say = words.join(' ')
|
|
21
|
+
} else if (args[i] === '--gif') {
|
|
22
|
+
flags.gif = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : 'meeb.gif'
|
|
23
|
+
} else if (args[i] === '--png') {
|
|
24
|
+
flags.png = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : 'meeb.png'
|
|
25
|
+
} else if (args[i] === '--svg') {
|
|
26
|
+
flags.svg = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : 'meeb.svg'
|
|
27
|
+
} else if (args[i] === '--label') {
|
|
28
|
+
const words = []
|
|
29
|
+
while (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
30
|
+
words.push(args[++i])
|
|
31
|
+
}
|
|
32
|
+
if (words.length) flags.label = words.join(' ')
|
|
33
|
+
} else if (args[i] === '--hat') {
|
|
34
|
+
flags.hat = true
|
|
35
|
+
} else if (args[i] === '--tiny') {
|
|
36
|
+
flags.tiny = true
|
|
37
|
+
} else if (args[i] === '--no-color') {
|
|
38
|
+
flags.noColor = true
|
|
39
|
+
} else if (args[i] === '--theme') {
|
|
40
|
+
flags.theme = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true
|
|
41
|
+
} else if (args[i] === '--pair') {
|
|
42
|
+
flags.pair = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true
|
|
43
|
+
} else if (args[i] === '--diff') {
|
|
44
|
+
flags.diff = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true
|
|
45
|
+
} else if (args[i] === '--animate') {
|
|
46
|
+
flags.animate = (args[i + 1] && !args[i + 1].startsWith('--')) ? args[++i] : true
|
|
47
|
+
} else if (args[i].startsWith('--')) {
|
|
48
|
+
flags[args[i].slice(2)] = true
|
|
49
|
+
} else {
|
|
50
|
+
positional.push(args[i])
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Treat bare subcommand words in positional as flags
|
|
55
|
+
// e.g. "meeb mouse say hi" => animal=mouse, flags.say="hi"
|
|
56
|
+
const flagWords = {
|
|
57
|
+
say: 'say', says: 'say', saying: 'say', info: 'info', pair: 'pair', diff: 'diff',
|
|
58
|
+
tiny: 'tiny', hat: 'hat', flip: 'flip', fortune: 'fortune',
|
|
59
|
+
theme: 'theme', animate: 'animate', motd: 'motd', calendar: 'calendar',
|
|
60
|
+
commit: 'commit', ci: 'ci', gif: 'gif', png: 'png', svg: 'svg',
|
|
61
|
+
stretch: 'stretch', squish: 'squish', stretchv: 'stretchv', squishv: 'squishv',
|
|
62
|
+
white: 'white', label: 'label', delay: 'delay', slide: 'slide', fall: 'fall', shatter: 'shatter',
|
|
63
|
+
}
|
|
64
|
+
const cleaned = []
|
|
65
|
+
for (let i = 0; i < positional.length; i++) {
|
|
66
|
+
const word = positional[i].toLowerCase()
|
|
67
|
+
if (flagWords[word] && !flags[flagWords[word]]) {
|
|
68
|
+
const key = flagWords[word]
|
|
69
|
+
if (key === 'say') {
|
|
70
|
+
const words = []
|
|
71
|
+
while (i + 1 < positional.length && !flagWords[positional[i + 1]?.toLowerCase()]) {
|
|
72
|
+
words.push(positional[++i])
|
|
73
|
+
}
|
|
74
|
+
if (words.length) flags.say = words.join(' ')
|
|
75
|
+
} else if (key === 'label') {
|
|
76
|
+
const words = []
|
|
77
|
+
while (i + 1 < positional.length && !flagWords[positional[i + 1]?.toLowerCase()]) {
|
|
78
|
+
words.push(positional[++i])
|
|
79
|
+
}
|
|
80
|
+
if (words.length) flags.label = words.join(' ')
|
|
81
|
+
} else if (['tiny', 'hat', 'flip', 'fortune', 'motd', 'calendar', 'commit', 'ci', 'stretch', 'squish', 'stretchv', 'squishv', 'white', 'delay', 'slide', 'fall', 'shatter'].includes(key)) {
|
|
82
|
+
flags[key] = true
|
|
83
|
+
} else if (i + 1 < positional.length && !flagWords[positional[i + 1]?.toLowerCase()]) {
|
|
84
|
+
flags[key] = positional[++i]
|
|
85
|
+
} else {
|
|
86
|
+
flags[key] = true
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
cleaned.push(positional[i])
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
positional.length = 0
|
|
93
|
+
positional.push(...cleaned)
|
|
94
|
+
|
|
95
|
+
const cmdWords = ['all', 'random', 'browse', 'list', 'count', 'help']
|
|
96
|
+
const cmd = (cmdWords.includes(positional[0]) ? positional[0] : null)
|
|
97
|
+
|| Object.keys(flags).find(f => cmdWords.includes(f))
|
|
98
|
+
|| positional[0]
|
|
99
|
+
|
|
100
|
+
function output(art) {
|
|
101
|
+
if (flags.noColor) return stripAnsi(art)
|
|
102
|
+
return art
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function applyPipeline(art) {
|
|
106
|
+
if (flags.hat) art = addHat(art)
|
|
107
|
+
if (flags.theme) {
|
|
108
|
+
const { applyTheme } = require('./theme')
|
|
109
|
+
art = applyTheme(art, flags.theme)
|
|
110
|
+
}
|
|
111
|
+
if (flags.stretch) art = stretchArt(art)
|
|
112
|
+
if (flags.squish) art = squishArt(art)
|
|
113
|
+
if (flags.stretchv) art = stretchVArt(art)
|
|
114
|
+
if (flags.squishv) art = squishVArt(art)
|
|
115
|
+
if (flags.flip) art = flipArt(art)
|
|
116
|
+
if (flags.say) art = addSay(art, flags.say)
|
|
117
|
+
if (flags.label) art = addLabel(art, flags.label)
|
|
118
|
+
return art
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const HIDE = '\x1b[?25l'
|
|
122
|
+
const SHOW = '\x1b[?25h'
|
|
123
|
+
const RESET = '\x1b[0m'
|
|
124
|
+
const CLEAR = '\x1b[2J\x1b[H'
|
|
125
|
+
|
|
126
|
+
function parseArtCells(art) {
|
|
127
|
+
const lines = art.split('\n')
|
|
128
|
+
const cells = []
|
|
129
|
+
let maxCol = 0
|
|
130
|
+
for (let row = 0; row < lines.length; row++) {
|
|
131
|
+
const line = lines[row]
|
|
132
|
+
let currentStyle = ''
|
|
133
|
+
let col = 0
|
|
134
|
+
let i = 0
|
|
135
|
+
while (i < line.length) {
|
|
136
|
+
if (line[i] === '\x1b' && i + 1 < line.length && line[i + 1] === '[') {
|
|
137
|
+
let j = i + 2
|
|
138
|
+
while (j < line.length && line[j] !== 'm') j++
|
|
139
|
+
const code = line.slice(i, j + 1)
|
|
140
|
+
if (code === '\x1b[0m') currentStyle = ''
|
|
141
|
+
else currentStyle += code
|
|
142
|
+
i = j + 1
|
|
143
|
+
} else {
|
|
144
|
+
if (line[i] !== ' ') {
|
|
145
|
+
cells.push({ row, col, style: currentStyle, char: line[i] })
|
|
146
|
+
if (col > maxCol) maxCol = col
|
|
147
|
+
}
|
|
148
|
+
col++
|
|
149
|
+
i++
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { cells, height: lines.length, width: maxCol + 1 }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function animSetup(label) {
|
|
157
|
+
process.stdout.write(HIDE + CLEAR)
|
|
158
|
+
let artStartRow = 1
|
|
159
|
+
if (label) {
|
|
160
|
+
process.stdout.write(output(label) + '\n')
|
|
161
|
+
artStartRow = 2
|
|
162
|
+
}
|
|
163
|
+
return artStartRow
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function animCleanup(height, artStartRow) {
|
|
167
|
+
process.stdout.write(`\x1b[${height + artStartRow};1H` + SHOW)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function drawCells(cells, artStartRow, offR, offC, termW, termH) {
|
|
171
|
+
let buf = ''
|
|
172
|
+
for (const c of cells) {
|
|
173
|
+
const r = c.row + artStartRow + (offR || 0)
|
|
174
|
+
const col = c.col + 1 + (offC || 0)
|
|
175
|
+
if (r < 1 || col < 1 || (termH && r > termH) || (termW && col > termW)) continue
|
|
176
|
+
buf += `\x1b[${r};${col}H${RESET}${c.style}${c.char}`
|
|
177
|
+
}
|
|
178
|
+
buf += RESET
|
|
179
|
+
process.stdout.write(buf)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function delayReveal(art, label) {
|
|
183
|
+
if (!process.stdout.isTTY) {
|
|
184
|
+
if (label) console.log(output(label))
|
|
185
|
+
console.log(output(art))
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { cells, height } = parseArtCells(art)
|
|
190
|
+
|
|
191
|
+
// Fisher-Yates shuffle
|
|
192
|
+
for (let i = cells.length - 1; i > 0; i--) {
|
|
193
|
+
const j = Math.floor(Math.random() * (i + 1))
|
|
194
|
+
;[cells[i], cells[j]] = [cells[j], cells[i]]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const artStartRow = animSetup(label)
|
|
198
|
+
|
|
199
|
+
const totalSteps = 20
|
|
200
|
+
const batchSize = Math.max(1, Math.ceil(cells.length / totalSteps))
|
|
201
|
+
let idx = 0
|
|
202
|
+
|
|
203
|
+
function step() {
|
|
204
|
+
const end = Math.min(idx + batchSize, cells.length)
|
|
205
|
+
let buf = ''
|
|
206
|
+
for (let i = idx; i < end; i++) {
|
|
207
|
+
const c = cells[i]
|
|
208
|
+
buf += `\x1b[${c.row + artStartRow};${c.col + 1}H${RESET}${c.style}${c.char}`
|
|
209
|
+
}
|
|
210
|
+
buf += RESET
|
|
211
|
+
process.stdout.write(buf)
|
|
212
|
+
idx = end
|
|
213
|
+
if (idx < cells.length) {
|
|
214
|
+
setTimeout(step, 60)
|
|
215
|
+
} else {
|
|
216
|
+
animCleanup(height, artStartRow)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.on('SIGINT', () => { animCleanup(height, artStartRow); process.exit(0) })
|
|
221
|
+
step()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function slideReveal(art, label) {
|
|
225
|
+
if (!process.stdout.isTTY) {
|
|
226
|
+
if (label) console.log(output(label))
|
|
227
|
+
console.log(output(art))
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { cells, height, width } = parseArtCells(art)
|
|
232
|
+
const termW = process.stdout.columns || 80
|
|
233
|
+
const artStartRow = animSetup(label)
|
|
234
|
+
|
|
235
|
+
const startOffset = termW
|
|
236
|
+
const totalFrames = 18
|
|
237
|
+
let frame = 0
|
|
238
|
+
|
|
239
|
+
function step() {
|
|
240
|
+
const t = frame / (totalFrames - 1) // 0 to 1
|
|
241
|
+
// ease-out: decelerate as it arrives
|
|
242
|
+
const ease = 1 - Math.pow(1 - t, 3)
|
|
243
|
+
const offset = Math.round(startOffset * (1 - ease))
|
|
244
|
+
|
|
245
|
+
process.stdout.write(CLEAR)
|
|
246
|
+
if (label) {
|
|
247
|
+
process.stdout.write(`\x1b[1;1H${output(label)}`)
|
|
248
|
+
}
|
|
249
|
+
drawCells(cells, artStartRow, 0, offset, termW, null)
|
|
250
|
+
|
|
251
|
+
frame++
|
|
252
|
+
if (frame < totalFrames) {
|
|
253
|
+
setTimeout(step, 45)
|
|
254
|
+
} else {
|
|
255
|
+
animCleanup(height, artStartRow)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
process.on('SIGINT', () => { animCleanup(height, artStartRow); process.exit(0) })
|
|
260
|
+
step()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function fallReveal(art, label) {
|
|
264
|
+
if (!process.stdout.isTTY) {
|
|
265
|
+
if (label) console.log(output(label))
|
|
266
|
+
console.log(output(art))
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const { cells, height } = parseArtCells(art)
|
|
271
|
+
const termH = process.stdout.rows || 24
|
|
272
|
+
const artStartRow = animSetup(label)
|
|
273
|
+
|
|
274
|
+
const startOffset = -(height + artStartRow)
|
|
275
|
+
const totalFrames = 20
|
|
276
|
+
let frame = 0
|
|
277
|
+
|
|
278
|
+
function step() {
|
|
279
|
+
const t = frame / (totalFrames - 1)
|
|
280
|
+
// bounce ease: overshoot then settle
|
|
281
|
+
let ease
|
|
282
|
+
if (t < 0.7) {
|
|
283
|
+
ease = (t / 0.7)
|
|
284
|
+
} else if (t < 0.85) {
|
|
285
|
+
ease = 1 + 0.15 * Math.sin((t - 0.7) / 0.15 * Math.PI)
|
|
286
|
+
} else {
|
|
287
|
+
ease = 1
|
|
288
|
+
}
|
|
289
|
+
const offset = Math.round(startOffset * (1 - ease))
|
|
290
|
+
|
|
291
|
+
process.stdout.write(CLEAR)
|
|
292
|
+
if (label) {
|
|
293
|
+
process.stdout.write(`\x1b[1;1H${output(label)}`)
|
|
294
|
+
}
|
|
295
|
+
drawCells(cells, artStartRow, offset, 0, null, termH)
|
|
296
|
+
|
|
297
|
+
frame++
|
|
298
|
+
if (frame < totalFrames) {
|
|
299
|
+
setTimeout(step, 45)
|
|
300
|
+
} else {
|
|
301
|
+
animCleanup(height, artStartRow)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
process.on('SIGINT', () => { animCleanup(height, artStartRow); process.exit(0) })
|
|
306
|
+
step()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function shatterReveal(art, label) {
|
|
310
|
+
if (!process.stdout.isTTY) {
|
|
311
|
+
if (label) console.log(output(label))
|
|
312
|
+
console.log(output(art))
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const { cells, height, width } = parseArtCells(art)
|
|
317
|
+
const termW = process.stdout.columns || 80
|
|
318
|
+
const termH = process.stdout.rows || 24
|
|
319
|
+
const artStartRow = animSetup(label)
|
|
320
|
+
|
|
321
|
+
// Draw the full art first
|
|
322
|
+
drawCells(cells, artStartRow, 0, 0, null, null)
|
|
323
|
+
|
|
324
|
+
// After a pause, shatter
|
|
325
|
+
setTimeout(() => {
|
|
326
|
+
// Give each cell physics properties
|
|
327
|
+
const particles = cells.map(c => ({
|
|
328
|
+
x: c.col,
|
|
329
|
+
y: c.row,
|
|
330
|
+
vx: (Math.random() - 0.5) * 4,
|
|
331
|
+
vy: -(Math.random() * 3 + 1),
|
|
332
|
+
style: c.style,
|
|
333
|
+
char: c.char,
|
|
334
|
+
}))
|
|
335
|
+
|
|
336
|
+
const gravity = 0.35
|
|
337
|
+
let frame = 0
|
|
338
|
+
const maxFrames = 40
|
|
339
|
+
|
|
340
|
+
function step() {
|
|
341
|
+
process.stdout.write(CLEAR)
|
|
342
|
+
if (label) {
|
|
343
|
+
process.stdout.write(`\x1b[1;1H${output(label)}`)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let anyVisible = false
|
|
347
|
+
let buf = ''
|
|
348
|
+
for (const p of particles) {
|
|
349
|
+
p.vy += gravity
|
|
350
|
+
p.x += p.vx
|
|
351
|
+
p.y += p.vy
|
|
352
|
+
|
|
353
|
+
const r = Math.round(p.y) + artStartRow
|
|
354
|
+
const col = Math.round(p.x) + 1
|
|
355
|
+
if (r >= 1 && r <= termH && col >= 1 && col <= termW) {
|
|
356
|
+
buf += `\x1b[${r};${col}H${RESET}${p.style}${p.char}`
|
|
357
|
+
anyVisible = true
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
buf += RESET
|
|
361
|
+
process.stdout.write(buf)
|
|
362
|
+
|
|
363
|
+
frame++
|
|
364
|
+
if (frame < maxFrames && anyVisible) {
|
|
365
|
+
setTimeout(step, 40)
|
|
366
|
+
} else {
|
|
367
|
+
process.stdout.write(CLEAR)
|
|
368
|
+
animCleanup(0, 1)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
step()
|
|
373
|
+
}, 800)
|
|
374
|
+
|
|
375
|
+
process.on('SIGINT', () => { process.stdout.write(CLEAR); animCleanup(0, 1); process.exit(0) })
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function exportOrPrint(art, label) {
|
|
379
|
+
const bgColor = flags.white ? [255, 255, 255] : undefined
|
|
380
|
+
const exported = []
|
|
381
|
+
if (flags.gif) {
|
|
382
|
+
const { generateGif } = require('./gif')
|
|
383
|
+
const path = typeof flags.gif === 'string' ? flags.gif : 'meeb.gif'
|
|
384
|
+
generateGif(art, path, bgColor)
|
|
385
|
+
exported.push(path)
|
|
386
|
+
}
|
|
387
|
+
if (flags.png) {
|
|
388
|
+
const { generatePng } = require('./png')
|
|
389
|
+
const path = typeof flags.png === 'string' ? flags.png : 'meeb.png'
|
|
390
|
+
generatePng(art, path, bgColor)
|
|
391
|
+
exported.push(path)
|
|
392
|
+
}
|
|
393
|
+
if (flags.svg) {
|
|
394
|
+
const { generateSvg } = require('./svg')
|
|
395
|
+
const path = typeof flags.svg === 'string' ? flags.svg : 'meeb.svg'
|
|
396
|
+
generateSvg(art, path, bgColor)
|
|
397
|
+
exported.push(path)
|
|
398
|
+
}
|
|
399
|
+
if (exported.length > 0) {
|
|
400
|
+
console.log(` saved to ${exported.join(', ')}`)
|
|
401
|
+
} else if (flags.delay) {
|
|
402
|
+
delayReveal(output(art), label ? output(label) : null)
|
|
403
|
+
} else if (flags.slide) {
|
|
404
|
+
slideReveal(output(art), label ? output(label) : null)
|
|
405
|
+
} else if (flags.fall) {
|
|
406
|
+
fallReveal(output(art), label ? output(label) : null)
|
|
407
|
+
} else if (flags.shatter) {
|
|
408
|
+
shatterReveal(output(art), label ? output(label) : null)
|
|
409
|
+
} else {
|
|
410
|
+
if (label) console.log(output(label))
|
|
411
|
+
console.log(output(art))
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function levenshtein(a, b) {
|
|
416
|
+
const m = a.length, n = b.length
|
|
417
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1))
|
|
418
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i
|
|
419
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j
|
|
420
|
+
for (let i = 1; i <= m; i++)
|
|
421
|
+
for (let j = 1; j <= n; j++)
|
|
422
|
+
dp[i][j] = Math.min(
|
|
423
|
+
dp[i - 1][j] + 1,
|
|
424
|
+
dp[i][j - 1] + 1,
|
|
425
|
+
dp[i - 1][j - 1] + (a[i - 1] !== b[j - 1] ? 1 : 0)
|
|
426
|
+
)
|
|
427
|
+
return dp[m][n]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function resolveAnimal(name) {
|
|
431
|
+
const resolved = emojiResolve(name)
|
|
432
|
+
let animal = animals[resolved || name]
|
|
433
|
+
if (animal) return animal
|
|
434
|
+
|
|
435
|
+
const input = name.toLowerCase()
|
|
436
|
+
const names = Object.keys(animals)
|
|
437
|
+
|
|
438
|
+
const sub = names.find(n => n.startsWith(input) || input.startsWith(n))
|
|
439
|
+
|| names.find(n => n.includes(input) || input.includes(n))
|
|
440
|
+
|
|
441
|
+
if (sub) return animals[sub]
|
|
442
|
+
|
|
443
|
+
let best = null, bestD = Infinity
|
|
444
|
+
for (const n of names) {
|
|
445
|
+
const d = levenshtein(input, n)
|
|
446
|
+
if (d < bestD) { bestD = d; best = n }
|
|
447
|
+
}
|
|
448
|
+
if (best && bestD <= Math.max(2, Math.ceil(input.length / 3))) {
|
|
449
|
+
return animals[best]
|
|
450
|
+
}
|
|
451
|
+
return null
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function getRandomAnimal() {
|
|
455
|
+
const keys = Object.keys(visible)
|
|
456
|
+
return visible[keys[Math.floor(Math.random() * keys.length)]]
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function renderAnimal(animal) {
|
|
460
|
+
return flags.tiny && animal.tiny ? animal.tiny() : animal.render()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// --- Pipe stdin support ---
|
|
464
|
+
// If stdin is piped, read it as --say message
|
|
465
|
+
function readStdin() {
|
|
466
|
+
if (process.stdin.isTTY) return null
|
|
467
|
+
try {
|
|
468
|
+
const fs = require('fs')
|
|
469
|
+
const input = fs.readFileSync(0, 'utf8').trim()
|
|
470
|
+
return input || null
|
|
471
|
+
} catch {
|
|
472
|
+
return null
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const stdinMsg = readStdin()
|
|
477
|
+
if (stdinMsg && !flags.say) {
|
|
478
|
+
flags.say = stdinMsg
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- Help ---
|
|
482
|
+
if ((!cmd && Object.keys(flags).length === 0) || cmd === 'help') {
|
|
483
|
+
console.log('')
|
|
484
|
+
console.log(rgb(215, 119, 87, ' meeb') + ' — cute ascii animals for your terminal')
|
|
485
|
+
console.log('')
|
|
486
|
+
console.log(' Usage: meeb <animal> [flags]')
|
|
487
|
+
console.log(' echo "hello" | meeb penguin')
|
|
488
|
+
console.log('')
|
|
489
|
+
console.log(' Animals:')
|
|
490
|
+
for (const name of Object.keys(visible)) {
|
|
491
|
+
console.log(` ${rgb(215, 119, 87, '●')} ${name}`)
|
|
492
|
+
}
|
|
493
|
+
console.log('')
|
|
494
|
+
console.log(' Commands:')
|
|
495
|
+
console.log(' --all show all animals')
|
|
496
|
+
console.log(' --random show a random animal')
|
|
497
|
+
console.log(' --list print names only (for scripting)')
|
|
498
|
+
console.log(' --count print total number of animals')
|
|
499
|
+
console.log(' --browse interactive TUI to browse animals')
|
|
500
|
+
console.log('')
|
|
501
|
+
console.log(' Display:')
|
|
502
|
+
console.log(' --say "msg" speech bubble above the animal')
|
|
503
|
+
console.log(' --tiny compact 4-5 row version')
|
|
504
|
+
console.log(' --hat seasonal hat (auto-detects month)')
|
|
505
|
+
console.log(' --flip mirror the animal horizontally')
|
|
506
|
+
console.log(' --fortune random fortune as speech bubble')
|
|
507
|
+
console.log(' --theme <name> color theme (neon, pastel, retro, mono, matrix)')
|
|
508
|
+
console.log(' --label "text" subtitle text below the animal')
|
|
509
|
+
console.log(' --no-color strip ANSI codes (for piping)')
|
|
510
|
+
console.log('')
|
|
511
|
+
console.log(' Transforms:')
|
|
512
|
+
console.log(' --stretch stretch horizontally (wider)')
|
|
513
|
+
console.log(' --squish squish horizontally (narrower)')
|
|
514
|
+
console.log(' --stretchv stretch vertically (taller)')
|
|
515
|
+
console.log(' --squishv squish vertically (shorter)')
|
|
516
|
+
console.log('')
|
|
517
|
+
console.log(' Animations:')
|
|
518
|
+
console.log(' --delay pixels materialize randomly')
|
|
519
|
+
console.log(' --slide slide in from the right')
|
|
520
|
+
console.log(' --fall drop from the top with a bounce')
|
|
521
|
+
console.log(' --shatter appear then explode with gravity')
|
|
522
|
+
console.log('')
|
|
523
|
+
console.log(' Features:')
|
|
524
|
+
console.log(' --motd message of the day (for .bashrc/.zshrc)')
|
|
525
|
+
console.log(' --pair <animal> show two animals side by side')
|
|
526
|
+
console.log(' --diff <animal> compare two animals side by side')
|
|
527
|
+
console.log(' --animate <animal> terminal animation (3 frames)')
|
|
528
|
+
console.log(' --info <animal> fun fact about the animal')
|
|
529
|
+
console.log(' --calendar animal of the day')
|
|
530
|
+
console.log(' --commit git commit message as speech bubble')
|
|
531
|
+
console.log(' --ci markdown-safe output (no ANSI)')
|
|
532
|
+
console.log('')
|
|
533
|
+
console.log(' Export:')
|
|
534
|
+
console.log(' --gif [path] save as gif (default: meeb.gif)')
|
|
535
|
+
console.log(' --png [path] save as png (default: meeb.png)')
|
|
536
|
+
console.log(' --svg [path] save as svg (default: meeb.svg)')
|
|
537
|
+
console.log(' --white white background for exports')
|
|
538
|
+
console.log('')
|
|
539
|
+
console.log(' Setup:')
|
|
540
|
+
console.log(' --install-completions install shell completions (bash/zsh/fish)')
|
|
541
|
+
console.log('')
|
|
542
|
+
console.log(' Emoji:')
|
|
543
|
+
console.log(' meeb 🐧 looks up the matching animal')
|
|
544
|
+
console.log('')
|
|
545
|
+
process.exit(0)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (cmd === 'list') {
|
|
549
|
+
console.log(Object.keys(visible).join('\n'))
|
|
550
|
+
process.exit(0)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (cmd === 'count') {
|
|
554
|
+
console.log(Object.keys(visible).length)
|
|
555
|
+
process.exit(0)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// --- --install-completions ---
|
|
559
|
+
if (flags['install-completions']) {
|
|
560
|
+
const fs = require('fs')
|
|
561
|
+
const path = require('path')
|
|
562
|
+
const os = require('os')
|
|
563
|
+
const shell = (process.env.SHELL || '').split('/').pop()
|
|
564
|
+
const completionsDir = path.join(__dirname, 'completions')
|
|
565
|
+
|
|
566
|
+
if (shell === 'zsh') {
|
|
567
|
+
const src = path.join(completionsDir, '_meeb')
|
|
568
|
+
const dest = path.join(os.homedir(), '.zsh', 'completions', '_meeb')
|
|
569
|
+
const destDir = path.dirname(dest)
|
|
570
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
|
|
571
|
+
fs.copyFileSync(src, dest)
|
|
572
|
+
console.log(` Installed zsh completions to ${dest}`)
|
|
573
|
+
console.log(` Add to .zshrc: fpath=(~/.zsh/completions $fpath)`)
|
|
574
|
+
console.log(` Then run: autoload -Uz compinit && compinit`)
|
|
575
|
+
} else if (shell === 'bash') {
|
|
576
|
+
const src = path.join(completionsDir, 'meeb.bash')
|
|
577
|
+
const dest = path.join(os.homedir(), '.bash_completion.d', 'meeb.bash')
|
|
578
|
+
const destDir = path.dirname(dest)
|
|
579
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
|
|
580
|
+
fs.copyFileSync(src, dest)
|
|
581
|
+
console.log(` Installed bash completions to ${dest}`)
|
|
582
|
+
console.log(` Add to .bashrc: source ~/.bash_completion.d/meeb.bash`)
|
|
583
|
+
} else if (shell === 'fish') {
|
|
584
|
+
const fishDir = path.join(os.homedir(), '.config', 'fish', 'completions')
|
|
585
|
+
if (!fs.existsSync(fishDir)) fs.mkdirSync(fishDir, { recursive: true })
|
|
586
|
+
const dest = path.join(fishDir, 'meeb.fish')
|
|
587
|
+
// generate fish completions
|
|
588
|
+
const animalNames = Object.keys(visible).join(' ')
|
|
589
|
+
const fishContent = [
|
|
590
|
+
'# meeb fish completions',
|
|
591
|
+
`complete -c meeb -f -a "${animalNames}"`,
|
|
592
|
+
'complete -c meeb -l all -d "show all animals"',
|
|
593
|
+
'complete -c meeb -l random -d "show a random animal"',
|
|
594
|
+
'complete -c meeb -l list -d "print names only"',
|
|
595
|
+
'complete -c meeb -l count -d "print total number"',
|
|
596
|
+
'complete -c meeb -l say -d "speech bubble" -r',
|
|
597
|
+
'complete -c meeb -l tiny -d "compact version"',
|
|
598
|
+
'complete -c meeb -l hat -d "seasonal hat"',
|
|
599
|
+
'complete -c meeb -l flip -d "mirror horizontally"',
|
|
600
|
+
'complete -c meeb -l fortune -d "random fortune"',
|
|
601
|
+
'complete -c meeb -l theme -d "color theme" -r -a "neon pastel retro mono matrix"',
|
|
602
|
+
'complete -c meeb -l motd -d "message of the day"',
|
|
603
|
+
'complete -c meeb -l pair -d "two animals side by side" -r',
|
|
604
|
+
'complete -c meeb -l diff -d "compare two animals" -r',
|
|
605
|
+
'complete -c meeb -l animate -d "terminal animation" -r',
|
|
606
|
+
'complete -c meeb -l info -d "fun animal fact" -r',
|
|
607
|
+
'complete -c meeb -l calendar -d "animal of the day"',
|
|
608
|
+
'complete -c meeb -l commit -d "git commit speech bubble"',
|
|
609
|
+
'complete -c meeb -l ci -d "markdown-safe output"',
|
|
610
|
+
'complete -c meeb -l gif -d "save as gif"',
|
|
611
|
+
'complete -c meeb -l png -d "save as png"',
|
|
612
|
+
'complete -c meeb -l svg -d "save as svg"',
|
|
613
|
+
'complete -c meeb -l browse -d "interactive TUI"',
|
|
614
|
+
'complete -c meeb -l no-color -d "strip ANSI codes"',
|
|
615
|
+
'complete -c meeb -l install-completions -d "install shell completions"',
|
|
616
|
+
].join('\n') + '\n'
|
|
617
|
+
fs.writeFileSync(dest, fishContent)
|
|
618
|
+
console.log(` Installed fish completions to ${dest}`)
|
|
619
|
+
} else {
|
|
620
|
+
console.log(` Unsupported shell: ${shell || '(unknown)'}`)
|
|
621
|
+
console.log(` Manual install: copy files from ${completionsDir}`)
|
|
622
|
+
}
|
|
623
|
+
process.exit(0)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// --fortune implies --say with a random fortune
|
|
627
|
+
if (flags.fortune && !flags.say) {
|
|
628
|
+
flags.say = getFortune()
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// --- --motd: compact random animal + fortune for shell startup ---
|
|
632
|
+
if (flags.motd) {
|
|
633
|
+
const animal = getRandomAnimal()
|
|
634
|
+
let art = animal.tiny ? animal.tiny() : animal.render()
|
|
635
|
+
art = addSay(art, getFortune())
|
|
636
|
+
if (flags.theme) {
|
|
637
|
+
const { applyTheme } = require('./theme')
|
|
638
|
+
art = applyTheme(art, flags.theme)
|
|
639
|
+
}
|
|
640
|
+
console.log(output(art))
|
|
641
|
+
process.exit(0)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// --- --calendar: deterministic animal of the day ---
|
|
645
|
+
if (flags.calendar) {
|
|
646
|
+
const now = new Date()
|
|
647
|
+
const dayOfYear = Math.floor((now - new Date(now.getFullYear(), 0, 0)) / 86400000)
|
|
648
|
+
const keys = Object.keys(visible)
|
|
649
|
+
const pick = keys[dayOfYear % keys.length]
|
|
650
|
+
const animal = visible[pick]
|
|
651
|
+
let art = renderAnimal(animal)
|
|
652
|
+
art = applyPipeline(art)
|
|
653
|
+
const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })
|
|
654
|
+
console.log(output(rgb(215, 119, 87, ` ── ${animal.name} ── `) + rgb(120, 120, 120, dateStr)))
|
|
655
|
+
console.log(output(art))
|
|
656
|
+
process.exit(0)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// --- --commit: git post-commit hook helper ---
|
|
660
|
+
if (flags.commit) {
|
|
661
|
+
const { execSync } = require('child_process')
|
|
662
|
+
let msg
|
|
663
|
+
try {
|
|
664
|
+
msg = execSync('git log -1 --pretty=%s 2>/dev/null', { encoding: 'utf8' }).trim()
|
|
665
|
+
} catch {
|
|
666
|
+
msg = 'committed!'
|
|
667
|
+
}
|
|
668
|
+
if (!flags.say) flags.say = msg
|
|
669
|
+
const animal = getRandomAnimal()
|
|
670
|
+
let art = renderAnimal(animal)
|
|
671
|
+
art = applyPipeline(art)
|
|
672
|
+
console.log(output(art))
|
|
673
|
+
process.exit(0)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// --- --ci: markdown-compatible output (no ANSI) ---
|
|
677
|
+
if (flags.ci) {
|
|
678
|
+
const animalName = typeof flags.ci === 'string' ? flags.ci : (positional[0] || null)
|
|
679
|
+
const animal = animalName ? resolveAnimal(animalName) : getRandomAnimal()
|
|
680
|
+
if (!animal) {
|
|
681
|
+
console.error(` unknown animal: "${animalName}"`)
|
|
682
|
+
process.exit(1)
|
|
683
|
+
}
|
|
684
|
+
let art = renderAnimal(animal)
|
|
685
|
+
if (flags.hat) art = addHat(art)
|
|
686
|
+
if (flags.flip) art = flipArt(art)
|
|
687
|
+
if (flags.say) art = addSay(art, flags.say)
|
|
688
|
+
// Output as markdown code block with no ANSI
|
|
689
|
+
console.log('```')
|
|
690
|
+
console.log(stripAnsi(art))
|
|
691
|
+
console.log('```')
|
|
692
|
+
process.exit(0)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// --- --info: fun animal fact ---
|
|
696
|
+
if (flags.info) {
|
|
697
|
+
const { getInfo } = require('./info')
|
|
698
|
+
const animalName = typeof flags.info === 'string' ? flags.info : (positional[0] || null)
|
|
699
|
+
const animal = animalName ? resolveAnimal(animalName) : getRandomAnimal()
|
|
700
|
+
if (!animal) {
|
|
701
|
+
console.error(` unknown animal: "${animalName}"`)
|
|
702
|
+
process.exit(1)
|
|
703
|
+
}
|
|
704
|
+
let art = renderAnimal(animal)
|
|
705
|
+
const fact = getInfo(animalName || Object.keys(animals).find(k => animals[k] === animal) || '')
|
|
706
|
+
if (!flags.say) flags.say = fact
|
|
707
|
+
art = applyPipeline(art)
|
|
708
|
+
console.log(output(rgb(215, 119, 87, ` ── ${animal.name} ──`)))
|
|
709
|
+
console.log(output(art))
|
|
710
|
+
process.exit(0)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// --- --pair: two animals side by side ---
|
|
714
|
+
if (flags.pair) {
|
|
715
|
+
const animal1Name = positional[0]
|
|
716
|
+
const animal2Name = typeof flags.pair === 'string' ? flags.pair : null
|
|
717
|
+
if (!animal1Name || !animal2Name) {
|
|
718
|
+
console.error(' Usage: meeb <animal1> --pair <animal2>')
|
|
719
|
+
process.exit(1)
|
|
720
|
+
}
|
|
721
|
+
const a1 = resolveAnimal(animal1Name)
|
|
722
|
+
const a2 = resolveAnimal(animal2Name)
|
|
723
|
+
if (!a1) { console.error(` unknown animal: "${animal1Name}"`); process.exit(1) }
|
|
724
|
+
if (!a2) { console.error(` unknown animal: "${animal2Name}"`); process.exit(1) }
|
|
725
|
+
|
|
726
|
+
let art1 = renderAnimal(a1)
|
|
727
|
+
let art2 = renderAnimal(a2)
|
|
728
|
+
if (flags.hat) { art1 = addHat(art1); art2 = addHat(art2) }
|
|
729
|
+
if (flags.theme) {
|
|
730
|
+
const { applyTheme } = require('./theme')
|
|
731
|
+
art1 = applyTheme(art1, flags.theme)
|
|
732
|
+
art2 = applyTheme(art2, flags.theme)
|
|
733
|
+
}
|
|
734
|
+
const combined = sideBySide(art1, art2, 4)
|
|
735
|
+
console.log(output(rgb(215, 119, 87, ` ── ${a1.name} & ${a2.name} ──`)))
|
|
736
|
+
console.log(output(combined))
|
|
737
|
+
process.exit(0)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// --- --diff: compare two animals side by side (with labels) ---
|
|
741
|
+
if (flags.diff) {
|
|
742
|
+
const animal1Name = positional[0]
|
|
743
|
+
const animal2Name = typeof flags.diff === 'string' ? flags.diff : null
|
|
744
|
+
if (!animal1Name || !animal2Name) {
|
|
745
|
+
console.error(' Usage: meeb <animal1> --diff <animal2>')
|
|
746
|
+
process.exit(1)
|
|
747
|
+
}
|
|
748
|
+
const a1 = resolveAnimal(animal1Name)
|
|
749
|
+
const a2 = resolveAnimal(animal2Name)
|
|
750
|
+
if (!a1) { console.error(` unknown animal: "${animal1Name}"`); process.exit(1) }
|
|
751
|
+
if (!a2) { console.error(` unknown animal: "${animal2Name}"`); process.exit(1) }
|
|
752
|
+
|
|
753
|
+
let art1 = renderAnimal(a1)
|
|
754
|
+
let art2 = renderAnimal(a2)
|
|
755
|
+
if (flags.theme) {
|
|
756
|
+
const { applyTheme } = require('./theme')
|
|
757
|
+
art1 = applyTheme(art1, flags.theme)
|
|
758
|
+
art2 = applyTheme(art2, flags.theme)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Add labels above each
|
|
762
|
+
const label1 = rgb(215, 119, 87, ` ${a1.name}`)
|
|
763
|
+
const label2 = rgb(215, 119, 87, ` ${a2.name}`)
|
|
764
|
+
art1 = label1 + '\n' + art1
|
|
765
|
+
art2 = label2 + '\n' + art2
|
|
766
|
+
|
|
767
|
+
const combined = sideBySide(art1, art2, 6)
|
|
768
|
+
console.log(output(combined))
|
|
769
|
+
process.exit(0)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// --- --animate: terminal animation ---
|
|
773
|
+
if (flags.animate) {
|
|
774
|
+
const animalName = typeof flags.animate === 'string' ? flags.animate : (positional[0] || null)
|
|
775
|
+
const animal = animalName ? resolveAnimal(animalName) : getRandomAnimal()
|
|
776
|
+
if (!animal) {
|
|
777
|
+
console.error(` unknown animal: "${animalName}"`)
|
|
778
|
+
process.exit(1)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (!process.stdout.isTTY) {
|
|
782
|
+
console.error(' --animate requires an interactive terminal')
|
|
783
|
+
process.exit(1)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const clear = '\x1b[2J\x1b[H'
|
|
787
|
+
const hide = '\x1b[?25l'
|
|
788
|
+
const show = '\x1b[?25h'
|
|
789
|
+
|
|
790
|
+
// Generate frames — normal, eyes closed (replace ◉/● with ─), normal
|
|
791
|
+
const base = renderAnimal(animal)
|
|
792
|
+
const blink = base
|
|
793
|
+
.replace(/◉/g, '─')
|
|
794
|
+
.replace(/●/g, '─')
|
|
795
|
+
.replace(/◆/g, '─')
|
|
796
|
+
|
|
797
|
+
let frames = [base, base, base, blink, base, base]
|
|
798
|
+
if (flags.hat) frames = frames.map(f => addHat(f))
|
|
799
|
+
if (flags.theme) {
|
|
800
|
+
const { applyTheme } = require('./theme')
|
|
801
|
+
frames = frames.map(f => applyTheme(f, flags.theme))
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const label = rgb(215, 119, 87, ` ── ${animal.name} ──`)
|
|
805
|
+
|
|
806
|
+
process.stdout.write(hide)
|
|
807
|
+
let frame = 0
|
|
808
|
+
const interval = setInterval(() => {
|
|
809
|
+
process.stdout.write(clear + label + '\n' + frames[frame % frames.length] + '\n')
|
|
810
|
+
frame++
|
|
811
|
+
}, 400)
|
|
812
|
+
|
|
813
|
+
// Run for 6 seconds then stop
|
|
814
|
+
setTimeout(() => {
|
|
815
|
+
clearInterval(interval)
|
|
816
|
+
process.stdout.write(show)
|
|
817
|
+
process.exit(0)
|
|
818
|
+
}, 6000)
|
|
819
|
+
|
|
820
|
+
// Clean exit on ctrl-c
|
|
821
|
+
process.on('SIGINT', () => {
|
|
822
|
+
clearInterval(interval)
|
|
823
|
+
process.stdout.write(show + clear)
|
|
824
|
+
process.exit(0)
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
// prevent falling through
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// --browse launches TUI
|
|
832
|
+
if (cmd === 'browse' || flags.browse) {
|
|
833
|
+
const { browse } = require('./tui')
|
|
834
|
+
browse(visible)
|
|
835
|
+
// browse takes over — no further code runs
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (cmd === 'random') {
|
|
839
|
+
const animal = getRandomAnimal()
|
|
840
|
+
let art = renderAnimal(animal)
|
|
841
|
+
art = applyPipeline(art)
|
|
842
|
+
exportOrPrint(art, rgb(215, 119, 87, ` ── ${animal.name} ──`))
|
|
843
|
+
process.exit(0)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (cmd === 'all') {
|
|
847
|
+
for (const animal of Object.values(visible)) {
|
|
848
|
+
let art = renderAnimal(animal)
|
|
849
|
+
if (flags.hat) art = addHat(art)
|
|
850
|
+
if (flags.flip) art = flipArt(art)
|
|
851
|
+
if (flags.theme) {
|
|
852
|
+
const { applyTheme } = require('./theme')
|
|
853
|
+
art = applyTheme(art, flags.theme)
|
|
854
|
+
}
|
|
855
|
+
exportOrPrint(art, rgb(215, 119, 87, ` ── ${animal.name} ──`))
|
|
856
|
+
}
|
|
857
|
+
process.exit(0)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// no command and no animal? pick random if we have modifier flags, else show help
|
|
861
|
+
if (!cmd) {
|
|
862
|
+
if (Object.keys(flags).length > 0) {
|
|
863
|
+
const keys = Object.keys(animals)
|
|
864
|
+
const pick = keys[Math.floor(Math.random() * keys.length)]
|
|
865
|
+
const animal = animals[pick]
|
|
866
|
+
let art = renderAnimal(animal)
|
|
867
|
+
art = applyPipeline(art)
|
|
868
|
+
exportOrPrint(art, rgb(215, 119, 87, ` ── ${animal.name} ──`))
|
|
869
|
+
process.exit(0)
|
|
870
|
+
}
|
|
871
|
+
process.exit(0)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// try emoji lookup, then fuzzy match
|
|
875
|
+
const resolved = emojiResolve(cmd)
|
|
876
|
+
let animal = animals[resolved || cmd]
|
|
877
|
+
if (!animal) {
|
|
878
|
+
const input = cmd.toLowerCase()
|
|
879
|
+
const names = Object.keys(animals)
|
|
880
|
+
|
|
881
|
+
// try substring match first (handles "croc" → "crocodile", "jelly" → "jellyfish")
|
|
882
|
+
const sub = names.find(n => n.startsWith(input) || input.startsWith(n))
|
|
883
|
+
|| names.find(n => n.includes(input) || input.includes(n))
|
|
884
|
+
|
|
885
|
+
if (sub) {
|
|
886
|
+
console.error(` did you mean ${rgb(215, 119, 87, sub)}?`)
|
|
887
|
+
animal = animals[sub]
|
|
888
|
+
} else {
|
|
889
|
+
// fuzzy match via levenshtein
|
|
890
|
+
let best = null, bestD = Infinity
|
|
891
|
+
for (const name of names) {
|
|
892
|
+
const d = levenshtein(input, name)
|
|
893
|
+
if (d < bestD) { bestD = d; best = name }
|
|
894
|
+
}
|
|
895
|
+
if (best && bestD <= Math.max(2, Math.ceil(input.length / 3))) {
|
|
896
|
+
console.error(` did you mean ${rgb(215, 119, 87, best)}?`)
|
|
897
|
+
animal = animals[best]
|
|
898
|
+
} else {
|
|
899
|
+
console.error(` unknown animal: "${cmd}"`)
|
|
900
|
+
console.error(` try: meeb --list`)
|
|
901
|
+
process.exit(1)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
let art = renderAnimal(animal)
|
|
907
|
+
art = applyPipeline(art)
|
|
908
|
+
exportOrPrint(art)
|
|
909
|
+
|
|
910
|
+
// Easter egg: stretched snail is the meeb logo
|
|
911
|
+
const animalKey = Object.keys(animals).find(k => animals[k] === animal)
|
|
912
|
+
if (animalKey === 'snail' && flags.stretch) {
|
|
913
|
+
console.log('')
|
|
914
|
+
console.log(rgb(215, 119, 87, ' AHA! THE MEEB LOGO! WHAT A WONDERFUL SIGHT!'))
|
|
915
|
+
}
|