nerdflirt 1.0.0 → 1.0.2
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/bin/nerdflirt.js +281 -26
- package/package.json +1 -1
package/bin/nerdflirt.js
CHANGED
|
@@ -14,35 +14,231 @@ const NAMES = [
|
|
|
14
14
|
'PacketPirate', 'RecurseRaven', 'ShellShark', 'TokenTiger', 'VoidViper'
|
|
15
15
|
]
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const BANNER = `
|
|
18
|
+
███╗ ██╗███████╗██████╗ ██████╗ ███████╗██╗ ██╗██████╗ ████████╗
|
|
19
|
+
████╗ ██║██╔════╝██╔══██╗██╔══██╗██╔════╝██║ ██║██╔══██╗╚══██╔══╝
|
|
20
|
+
██╔██╗ ██║█████╗ ██████╔╝██║ ██║█████╗ ██║ ██║██████╔╝ ██║
|
|
21
|
+
██║╚██╗██║██╔══╝ ██╔══██╗██║ ██║██╔══╝ ██║ ██║██╔══██╗ ██║
|
|
22
|
+
██║ ╚████║███████╗██║ ██║██████╔╝██║ ███████╗██║██║ ██║ ██║
|
|
23
|
+
╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚══════╝╚═╝╚═╝ ╚═╝ ╚═╝
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
const COLORS = [chalk.magenta, chalk.hex('#FF6B9D'), chalk.hex('#C084FC'), chalk.hex('#F472B6'), chalk.hex('#818CF8'), chalk.hex('#FB7185')]
|
|
27
|
+
|
|
28
|
+
function animateBanner() {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const lines = BANNER.split('\n').filter(Boolean)
|
|
31
|
+
let i = 0
|
|
32
|
+
const interval = setInterval(() => {
|
|
33
|
+
if (i >= lines.length) { clearInterval(interval); console.log(); resolve(); return }
|
|
34
|
+
console.log(COLORS[i % COLORS.length](lines[i]))
|
|
35
|
+
i++
|
|
36
|
+
}, 80)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function startLoader(text) {
|
|
41
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
42
|
+
let i = 0
|
|
43
|
+
let stopped = false
|
|
44
|
+
const id = setInterval(() => {
|
|
45
|
+
const frame = chalk.hex('#C084FC')(frames[i++ % frames.length]) + ' ' + chalk.dim(text)
|
|
46
|
+
// save cursor, move up to loader line, clear it, write frame, restore cursor
|
|
47
|
+
process.stdout.write('\x1b[s\x1b[A\x1b[2K\r ' + frame + '\x1b[u')
|
|
48
|
+
}, 80)
|
|
49
|
+
return () => {
|
|
50
|
+
if (stopped) return
|
|
51
|
+
stopped = true
|
|
52
|
+
clearInterval(id)
|
|
53
|
+
process.stdout.write('\x1b[s\x1b[A\x1b[2K\x1b[u')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- State ---
|
|
58
|
+
let myName = NAMES[Math.floor(Math.random() * NAMES.length)]
|
|
59
|
+
const peers = new Map() // id -> { conn, name, liked }
|
|
60
|
+
const likedMe = new Set() // peer ids that swiped right on us
|
|
61
|
+
let matchedPeer = null // id of current chat partner
|
|
62
|
+
let mode = 'lobby' // 'lobby' | 'swiping' | 'chat'
|
|
63
|
+
let swipeQueue = [] // peer ids to swipe through
|
|
64
|
+
let swipeIndex = 0
|
|
65
|
+
|
|
19
66
|
const swarm = new Hyperswarm()
|
|
20
67
|
|
|
68
|
+
// --- Helpers ---
|
|
69
|
+
function send(id, obj) {
|
|
70
|
+
peers.get(id)?.conn.write(JSON.stringify(obj))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function broadcast(obj) {
|
|
74
|
+
const msg = JSON.stringify(obj)
|
|
75
|
+
for (const { conn } of peers.values()) conn.write(msg)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function showSwipeCard() {
|
|
79
|
+
// rebuild queue from unmatched, named peers we haven't liked yet
|
|
80
|
+
swipeQueue = [...peers.entries()]
|
|
81
|
+
.filter(([id, p]) => p.name && !p.liked && id !== matchedPeer)
|
|
82
|
+
.map(([id]) => id)
|
|
83
|
+
|
|
84
|
+
if (!swipeQueue.length) {
|
|
85
|
+
console.log(chalk.dim('\nNo nerds to swipe on right now. Waiting for more to join...\n'))
|
|
86
|
+
mode = 'lobby'
|
|
87
|
+
rl.prompt()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
swipeIndex = swipeIndex % swipeQueue.length
|
|
92
|
+
const id = swipeQueue[swipeIndex]
|
|
93
|
+
const name = peers.get(id)?.name || '???'
|
|
94
|
+
|
|
95
|
+
console.log(chalk.hex('#C084FC')(`
|
|
96
|
+
┌─────────────────────────────┐
|
|
97
|
+
│ │
|
|
98
|
+
│ ${chalk.bold.hex('#F472B6')(name).padEnd(35)}│
|
|
99
|
+
│ │
|
|
100
|
+
│ ${chalk.dim('→ swipe right (like)')} │
|
|
101
|
+
│ ${chalk.dim('← swipe left (skip)')} │
|
|
102
|
+
│ ${chalk.dim('q back to lobby')} │
|
|
103
|
+
│ │
|
|
104
|
+
└─────────────────────────────┘`))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function enterChat(id) {
|
|
108
|
+
matchedPeer = id
|
|
109
|
+
mode = 'chat'
|
|
110
|
+
const name = peers.get(id)?.name || '???'
|
|
111
|
+
console.log(chalk.hex('#F472B6')(`\n💘 It's a match! You and ${chalk.bold(name)} both swiped right!`))
|
|
112
|
+
console.log(chalk.dim(' Type to chat. /next to find someone new. /quit to exit.\n'))
|
|
113
|
+
rl.setPrompt(chalk.hex('#C084FC')(`${myName}> `))
|
|
114
|
+
rl.prompt()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Boot ---
|
|
118
|
+
await animateBanner()
|
|
119
|
+
console.log(chalk.hex('#F472B6')(` ♥ You are: ${chalk.bold(myName)}`))
|
|
120
|
+
console.log(chalk.dim(' /swipe to start swiping • /peers to see who\'s online • /quit to exit\n'))
|
|
121
|
+
|
|
21
122
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
123
|
+
rl.setPrompt(chalk.hex('#C084FC')(`${myName}> `))
|
|
124
|
+
|
|
125
|
+
console.log() // reserve line for loader
|
|
126
|
+
rl.prompt()
|
|
127
|
+
const stopLoader = startLoader('Searching for nerds on the network...')
|
|
128
|
+
|
|
129
|
+
let firstConnection = true
|
|
22
130
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
You are: ${chalk.bold(myName)}
|
|
26
|
-
Searching for nerds...
|
|
27
|
-
`))
|
|
131
|
+
// --- Raw keypress handling for swipe mode ---
|
|
132
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
28
133
|
|
|
134
|
+
function onKeypress(buf) {
|
|
135
|
+
if (mode !== 'swiping') return
|
|
136
|
+
|
|
137
|
+
const key = buf.toString()
|
|
138
|
+
|
|
139
|
+
// arrow right = like
|
|
140
|
+
if (key === '\u001b[C') {
|
|
141
|
+
const id = swipeQueue[swipeIndex]
|
|
142
|
+
if (!id || !peers.has(id)) { showSwipeCard(); return }
|
|
143
|
+
peers.get(id).liked = true
|
|
144
|
+
const name = peers.get(id)?.name || '???'
|
|
145
|
+
console.log(chalk.hex('#4ADE80')(` 💚 You liked ${name}`))
|
|
146
|
+
send(id, { type: 'like' })
|
|
147
|
+
|
|
148
|
+
if (likedMe.has(id)) {
|
|
149
|
+
disableRawSwipe()
|
|
150
|
+
enterChat(id)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
swipeIndex++
|
|
154
|
+
showSwipeCard()
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// arrow left = skip
|
|
159
|
+
if (key === '\u001b[D') {
|
|
160
|
+
const id = swipeQueue[swipeIndex]
|
|
161
|
+
const name = peers.get(id)?.name || '???'
|
|
162
|
+
console.log(chalk.hex('#FB7185')(` 💨 Skipped ${name}`))
|
|
163
|
+
swipeIndex++
|
|
164
|
+
showSwipeCard()
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// q = back to lobby
|
|
169
|
+
if (key === 'q' || key === 'Q') {
|
|
170
|
+
disableRawSwipe()
|
|
171
|
+
mode = 'lobby'
|
|
172
|
+
console.log(chalk.dim('\nBack to lobby.\n'))
|
|
173
|
+
rl.prompt()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ctrl-c
|
|
178
|
+
if (key === '\x03') {
|
|
179
|
+
swarm.destroy()
|
|
180
|
+
process.exit(0)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function enableRawSwipe() {
|
|
185
|
+
if (process.stdin.isTTY) {
|
|
186
|
+
rl.pause()
|
|
187
|
+
process.stdin.setRawMode(true)
|
|
188
|
+
process.stdin.resume()
|
|
189
|
+
process.stdin.on('data', onKeypress)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function disableRawSwipe() {
|
|
194
|
+
if (process.stdin.isTTY) {
|
|
195
|
+
process.stdin.removeListener('data', onKeypress)
|
|
196
|
+
process.stdin.setRawMode(false)
|
|
197
|
+
rl.resume()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Swarm events ---
|
|
29
202
|
swarm.on('connection', (conn, info) => {
|
|
203
|
+
if (firstConnection) {
|
|
204
|
+
stopLoader()
|
|
205
|
+
firstConnection = false
|
|
206
|
+
rl.setPrompt(chalk.hex('#C084FC')(`${myName}> `))
|
|
207
|
+
rl.prompt()
|
|
208
|
+
}
|
|
209
|
+
|
|
30
210
|
const id = b4a.toString(info.publicKey, 'hex').slice(0, 8)
|
|
31
|
-
peers.set(id, { conn, name: null })
|
|
211
|
+
peers.set(id, { conn, name: null, liked: false })
|
|
32
212
|
|
|
33
|
-
// send our name on connect
|
|
34
213
|
conn.write(JSON.stringify({ type: 'name', name: myName }))
|
|
35
214
|
|
|
36
215
|
conn.on('data', (data) => {
|
|
37
216
|
try {
|
|
38
217
|
const msg = JSON.parse(data.toString())
|
|
218
|
+
|
|
39
219
|
if (msg.type === 'name') {
|
|
40
|
-
peers.get(id)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
220
|
+
const old = peers.get(id)?.name
|
|
221
|
+
if (peers.has(id)) peers.get(id).name = msg.name
|
|
222
|
+
if (old) {
|
|
223
|
+
console.log(chalk.hex('#FBBF24')(`\n✏️ ${old} is now ${msg.name}\n`))
|
|
224
|
+
} else {
|
|
225
|
+
console.log(chalk.hex('#4ADE80')(`\n💚 ${msg.name} joined the network!\n`))
|
|
226
|
+
}
|
|
227
|
+
if (mode !== 'swiping') rl.prompt(true)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (msg.type === 'like') {
|
|
231
|
+
likedMe.add(id)
|
|
232
|
+
// if we already liked them back, it's a match
|
|
233
|
+
if (peers.get(id)?.liked) {
|
|
234
|
+
if (mode === 'swiping') disableRawSwipe()
|
|
235
|
+
enterChat(id)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (msg.type === 'chat' && matchedPeer === id) {
|
|
44
240
|
const peerName = peers.get(id)?.name || id
|
|
45
|
-
console.log(chalk.
|
|
241
|
+
console.log(chalk.hex('#22D3EE')(`\n${peerName}: `) + chalk.white(msg.text))
|
|
46
242
|
rl.prompt(true)
|
|
47
243
|
}
|
|
48
244
|
} catch {}
|
|
@@ -50,41 +246,100 @@ swarm.on('connection', (conn, info) => {
|
|
|
50
246
|
|
|
51
247
|
conn.on('close', () => {
|
|
52
248
|
const name = peers.get(id)?.name || id
|
|
53
|
-
|
|
249
|
+
likedMe.delete(id)
|
|
54
250
|
peers.delete(id)
|
|
55
|
-
|
|
251
|
+
if (matchedPeer === id) {
|
|
252
|
+
matchedPeer = null
|
|
253
|
+
mode = 'lobby'
|
|
254
|
+
console.log(chalk.hex('#FB7185')(`\n💔 ${name} disconnected. Back to lobby.\n`))
|
|
255
|
+
} else {
|
|
256
|
+
console.log(chalk.hex('#FB7185')(`\n💔 ${name} left the network.\n`))
|
|
257
|
+
}
|
|
258
|
+
if (mode !== 'swiping') rl.prompt(true)
|
|
56
259
|
})
|
|
57
260
|
|
|
58
|
-
conn.on('error', () => peers.delete(id))
|
|
261
|
+
conn.on('error', () => { likedMe.delete(id); peers.delete(id) })
|
|
59
262
|
})
|
|
60
263
|
|
|
61
264
|
const discovery = swarm.join(TOPIC, { client: true, server: true })
|
|
62
265
|
await discovery.flushed()
|
|
63
266
|
|
|
64
|
-
|
|
65
|
-
rl.prompt()
|
|
66
|
-
|
|
267
|
+
// --- Line input (lobby + chat modes) ---
|
|
67
268
|
rl.on('line', (line) => {
|
|
269
|
+
stopLoader()
|
|
68
270
|
const text = line.trim()
|
|
69
271
|
if (!text) { rl.prompt(); return }
|
|
70
272
|
|
|
71
273
|
if (text === '/quit') {
|
|
72
|
-
console.log(chalk.
|
|
274
|
+
console.log(chalk.hex('#F472B6')('\n♥ Until next time, nerd. ♥\n'))
|
|
73
275
|
swarm.destroy()
|
|
74
276
|
process.exit(0)
|
|
75
277
|
}
|
|
76
278
|
|
|
77
279
|
if (text === '/peers') {
|
|
78
|
-
const names = [...peers.values()].
|
|
79
|
-
console.log(chalk.
|
|
280
|
+
const names = [...peers.values()].filter(p => p.name).map(p => p.name)
|
|
281
|
+
console.log(chalk.hex('#FBBF24')(`Online: ${names.length ? names.join(', ') : 'nobody yet'}`))
|
|
282
|
+
rl.prompt()
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (text.startsWith('/editname')) {
|
|
287
|
+
const newName = text.slice(10).trim()
|
|
288
|
+
if (!newName) {
|
|
289
|
+
console.log(chalk.hex('#FB7185')('Usage: /editname <new name>'))
|
|
290
|
+
rl.prompt()
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
const taken = [...peers.values()].some(p => p.name?.toLowerCase() === newName.toLowerCase())
|
|
294
|
+
if (taken) {
|
|
295
|
+
console.log(chalk.hex('#FB7185')(`"${newName}" is already taken.`))
|
|
296
|
+
rl.prompt()
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
const oldName = myName
|
|
300
|
+
myName = newName
|
|
301
|
+
rl.setPrompt(chalk.hex('#C084FC')(`${myName}> `))
|
|
302
|
+
broadcast({ type: 'name', name: myName })
|
|
303
|
+
console.log(chalk.hex('#FBBF24')(`✏️ You are now ${chalk.bold(myName)} (was ${oldName})`))
|
|
80
304
|
rl.prompt()
|
|
81
305
|
return
|
|
82
306
|
}
|
|
83
307
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
308
|
+
if (text === '/swipe') {
|
|
309
|
+
if (matchedPeer) {
|
|
310
|
+
matchedPeer = null
|
|
311
|
+
console.log(chalk.dim('Left the chat.'))
|
|
312
|
+
}
|
|
313
|
+
mode = 'swiping'
|
|
314
|
+
swipeIndex = 0
|
|
315
|
+
enableRawSwipe()
|
|
316
|
+
showSwipeCard()
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (text === '/next') {
|
|
321
|
+
if (matchedPeer) {
|
|
322
|
+
send(matchedPeer, { type: 'chat', text: '👋 [left the chat]' })
|
|
323
|
+
matchedPeer = null
|
|
324
|
+
}
|
|
325
|
+
mode = 'swiping'
|
|
326
|
+
swipeIndex = 0
|
|
327
|
+
// reset likes so we can re-swipe
|
|
328
|
+
for (const p of peers.values()) p.liked = false
|
|
329
|
+
likedMe.clear()
|
|
330
|
+
enableRawSwipe()
|
|
331
|
+
showSwipeCard()
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// In chat mode, send to matched peer only
|
|
336
|
+
if (mode === 'chat' && matchedPeer) {
|
|
337
|
+
send(matchedPeer, { type: 'chat', text })
|
|
338
|
+
rl.prompt()
|
|
339
|
+
return
|
|
87
340
|
}
|
|
341
|
+
|
|
342
|
+
console.log(chalk.dim('Type /swipe to find someone, or /peers to see who\'s online.'))
|
|
88
343
|
rl.prompt()
|
|
89
344
|
})
|
|
90
345
|
|