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.
Files changed (2) hide show
  1. package/bin/nerdflirt.js +281 -26
  2. 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 myName = NAMES[Math.floor(Math.random() * NAMES.length)]
18
- const peers = new Map()
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
- console.log(chalk.magentaBright(`
24
- nerdflirt
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).name = msg.name
41
- console.log(chalk.green(`\n💚 ${msg.name} connected! Say something flirty.\n`))
42
- rl.prompt(true)
43
- } else if (msg.type === 'chat') {
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.cyan(`\n${peerName}: ${msg.text}`))
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
- console.log(chalk.red(`\n💔 ${name} disconnected.\n`))
249
+ likedMe.delete(id)
54
250
  peers.delete(id)
55
- rl.prompt(true)
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
- rl.setPrompt(chalk.magenta(`${myName}> `))
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.magentaBright('\n♥ Until next time, nerd. ♥\n'))
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()].map(p => p.name || '???')
79
- console.log(chalk.yellow(`Connected: ${names.length ? names.join(', ') : 'nobody yet'}`))
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
- const msg = JSON.stringify({ type: 'chat', text })
85
- for (const { conn } of peers.values()) {
86
- conn.write(msg)
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerdflirt",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Terminal flirting",
5
5
  "bin": {
6
6
  "nerdflirt": "./bin/nerdflirt.js"