@pdc-test/chat-io 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/cli.js CHANGED
@@ -25,8 +25,10 @@ const rl = readline.createInterface({
25
25
  let isTyping = false;
26
26
  let typingTimeout = null;
27
27
  let typingUsers = new Set();
28
+ let userTypingTimers = {};
28
29
  let typingFrames = ['.', '..', '...'];
29
30
  let tIdx = 0;
31
+ let isMultilinePrompt = false;
30
32
 
31
33
  // Escuchador de teclado constante (Raw Mode Mágico)
32
34
  process.stdin.on('keypress', (str, key) => {
@@ -42,26 +44,31 @@ process.stdin.on('keypress', (str, key) => {
42
44
  clearTimeout(typingTimeout);
43
45
  typingTimeout = setTimeout(() => {
44
46
  isTyping = false;
45
- }, 2000);
47
+ if (ws.readyState === WebSocket.OPEN) ws.send("/typ_stop");
48
+ }, 1500);
46
49
  }
47
50
  });
48
51
 
49
- // Renderizado de "Escribiendo..." Dinámico en la Barra Inferior
52
+ // Renderizado de "Escribiendo..." en la parte SUPERIOR (En el Prompt)
50
53
  setInterval(() => {
51
54
  if (typingUsers.size > 0) {
52
- readline.clearLine(process.stdout, 0);
53
- readline.cursorTo(process.stdout, 0);
54
-
55
55
  const users = Array.from(typingUsers).join(", ");
56
56
  const suffix = users.includes(",") ? "están escribiendo" : "está escribiendo";
57
57
 
58
- // Escribe el texto Gris y luego vuelve al inicio en 0 milisegundos para no interrumpir
59
- process.stdout.write(`\x1b[90m${users} ${suffix} ${typingFrames[tIdx]}\x1b[0m`);
58
+ rl.setPrompt(`\x1b[90m[${users} ${suffix} ${typingFrames[tIdx]}]\x1b[0m\n> `);
60
59
  tIdx = (tIdx + 1) % typingFrames.length;
61
-
62
- // Restaurar silenciosamente lo que el usuario estaba redactando
63
- readline.cursorTo(process.stdout, 0);
64
- process.stdout.write(`> ${rl.line}`);
60
+ rl.prompt(true);
61
+ isMultilinePrompt = true;
62
+ } else {
63
+ if (isMultilinePrompt) {
64
+ rl.setPrompt('> ');
65
+ // Borrar el fantasma superior que dejó el prompt multilinea
66
+ readline.moveCursor(process.stdout, 0, -1);
67
+ readline.clearLine(process.stdout, 0);
68
+ readline.moveCursor(process.stdout, 0, 1);
69
+ rl.prompt(true);
70
+ isMultilinePrompt = false;
71
+ }
65
72
  }
66
73
  }, 400);
67
74
 
@@ -79,27 +86,48 @@ ws.on('message', (data) => {
79
86
  if (msg.startsWith("/typ_event ")) {
80
87
  const u = msg.replace("/typ_event ", "").trim();
81
88
  typingUsers.add(u);
82
- setTimeout(() => typingUsers.delete(u), 3000); // 3seg de cooldown maximo
89
+ clearTimeout(userTypingTimers[u]);
90
+ userTypingTimers[u] = setTimeout(() => typingUsers.delete(u), 3500);
83
91
  return;
84
92
  }
85
93
 
86
- // Si recibimos texto normal, limpiamos indicadores para que no estorben
94
+ if (msg.startsWith("/typ_stop_event ")) {
95
+ const u = msg.replace("/typ_stop_event ", "").trim();
96
+ typingUsers.delete(u);
97
+ clearTimeout(userTypingTimers[u]);
98
+ return;
99
+ }
100
+
101
+ // Si recibimos texto normal, limpiamos todas las señales tácticas
87
102
  typingUsers.clear();
88
103
 
89
- // Purgamos la línea completa en la consola antes de dibujar la obra de Arte/Chat
104
+ // Purgamos toda visualización flotante o entrada corrupta antes de dibujar texto nuevo
105
+ if (isMultilinePrompt) {
106
+ readline.moveCursor(process.stdout, 0, -1);
107
+ readline.clearLine(process.stdout, 0);
108
+ readline.moveCursor(process.stdout, 0, 1);
109
+ isMultilinePrompt = false;
110
+ }
90
111
  readline.clearLine(process.stdout, 0);
91
112
  readline.cursorTo(process.stdout, 0);
92
113
 
93
- console.log(msg); // Imprimimos la maravilla de Colores que envió Railway
94
- rl.prompt(true); // Forzamos al cursor a volver al lugar de escritura limpio
114
+ console.log(msg);
115
+ rl.setPrompt('> ');
116
+ rl.prompt(true);
95
117
  });
96
118
 
97
119
  // Cuando le damos ENTER
98
120
  rl.on('line', (input) => {
121
+ // Borramos la línea que acabas de teclear para que no se duplique
122
+ // con el mensaje que el servidor nos devolverá en azul
123
+ readline.moveCursor(process.stdout, 0, -1);
124
+ readline.clearLine(process.stdout, 0);
125
+
99
126
  const line = input.trim();
100
127
  if (line) {
101
128
  ws.send(line);
102
- isTyping = false; // Matamos flag al presionar intro
129
+ isTyping = false;
130
+ ws.send("/typ_stop");
103
131
  }
104
132
  rl.prompt(true);
105
133
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdc-test/chat-io",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "main": "./src/index.js",
5
5
  "bin": {
6
6
  "chat-io": "bin/cli.js"
@@ -9,6 +9,7 @@
9
9
  "start": "node src/server/index.js"
10
10
  },
11
11
  "dependencies": {
12
+ "@pdc-test/chat-io": "^1.0.2",
12
13
  "crypto-js": "^4.2.0",
13
14
  "ws": "^8.0.0"
14
15
  }
@@ -62,7 +62,7 @@ wss.on("connection", (server) => {
62
62
 
63
63
  // REGISTRO ÚNICO
64
64
  if (!socketData.name) {
65
- if (!text) return;
65
+ if (!text || text === "/typ") return;
66
66
  const desiredName = text.replace("/name ", "").trim();
67
67
  if (targetExists(desiredName)) {
68
68
  server.send("❌ Nombre en uso. Escribe otro distinto:");
@@ -88,14 +88,14 @@ wss.on("connection", (server) => {
88
88
  const myEmoji = socketData.emoji;
89
89
 
90
90
  if (text === "/help") {
91
- server.send(`\n──────────── 💡 COMANDOS MÁGICOS ────────────
91
+ server.send(`\n──────────── 💡 COMANDOS ────────────
92
92
  👥 /users → Ver conectados
93
- 🪙 /flip → Lanzar moneda (Animado)
93
+ 🪙 /flip → Lanzar moneda
94
94
  🎮 /rps <usr> <j> → Piedra-Papel-Tijera
95
95
  🌟 /blink <msj> → Mensaje parpadeante
96
96
  🔒 /w <usr> <msj> → Mensaje privado
97
- 🏪 /tiendita → Animación ASCII Sorpresa
98
- ℹ️ /help → Esta ayuda
97
+ 🏪 /tiendita → Invitación a la Tiendita
98
+ ℹ️ /help → Esta ayuda
99
99
  ─────────────────────────────────────────────\n`);
100
100
  return;
101
101
  }
@@ -107,51 +107,73 @@ wss.on("connection", (server) => {
107
107
  return;
108
108
  }
109
109
 
110
- // Indicador de "Escribiendo" Exclusivo para Smart Clients
111
- if (text === "/typ") {
110
+ // Indicadores de "Escribiendo" Exclusivos para Smart Clients
111
+ if (text === "/typ" || text === "/typ_stop") {
112
112
  for (const [s, data] of sessions.entries()) {
113
113
  if (s !== server && s.readyState === WebSocket.OPEN && data.smart) {
114
- try { s.send(`/typ_event ${myEmoji} ${myName}`); } catch(e) {}
114
+ try { s.send(`${text}_event ${myEmoji} ${myName}`); } catch (e) { }
115
115
  }
116
116
  }
117
- return; // No lo procesamos más, es invisible
117
+ return; // Invisible, solo de control
118
118
  }
119
119
 
120
- // Mensajes Privados
120
+ // Restablecer chat a global
121
+ if (text === "/all") {
122
+ socketData.privateTarget = null;
123
+ server.send(`\n[Sistema] 🟢 \x1b[32mModo Chat Público Restaurado.\x1b[0m Ahora todos leerán tus mensajes.\n`);
124
+ return;
125
+ }
126
+
127
+ // Mensajes Privados Estáticos y Dinámicos
121
128
  if (text.startsWith("/w ")) {
122
129
  const parts = text.split(" ");
123
- if (parts.length < 3) { server.send("❌ Error. Uso: /w <usuario> <mensaje secreto>"); return; }
124
130
  const targetUser = parts[1];
125
- const secretMsg = parts.slice(2).join(" ");
126
131
 
127
132
  if (!targetExists(targetUser)) { server.send("❌ Usuario no encontrado."); return; }
128
133
  if (targetUser === myName) { server.send("❌ ¿Hablando contigo mismo?"); return; }
129
134
 
130
- server.send(`\n[${getTime()}] 🔒 [Secret ${targetUser}]: \x1b[35m${secretMsg}\x1b[0m`);
131
- sendToTarget(targetUser, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${secretMsg}\x1b[0m\n`);
135
+ // Si solo envían /w Mario (Se quedan en modo privado)
136
+ if (parts.length === 2) {
137
+ socketData.privateTarget = targetUser;
138
+ server.send(`\n[Sistema] 🔒 \x1b[35mModo Susurro activado con ${targetUser}.\x1b[0m Todo lo que escribas solo lo leerá él. Escribe /all para salir.\n`);
139
+ return;
140
+ }
141
+
142
+ // Si lo envían todo de golpe /w Mario hola
143
+ const secretMsg = parts.slice(2).join(" ");
144
+ server.send(`\n[${getTime()}] 🔒 [Private → ${targetUser}]: \x1b[35m${secretMsg}\x1b[0m`);
145
+ sendToTarget(targetUser, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${secretMsg}\x1b[0m`);
132
146
  return;
133
147
  }
134
148
 
135
- // Moneda Animada de 3 Segundos
149
+ // Moneda Animada ASCII Rotativa in-place
136
150
  if (text === "/flip") {
137
- server.send(`\n[${getTime()}] 🪙 Alistando la moneda...`);
138
- broadcast(`\n[${getTime()}] 🪙 [${myEmoji} ${myName}] prepara un lanzamiento de moneda...\n`, server);
151
+ const prep = `\n[${getTime()}] 🪙 [${myEmoji} ${myName}] lanza una moneda al aire...\n`;
152
+ server.send(prep);
153
+ broadcast(prep, server);
154
+
155
+ const asciiCoins = [
156
+ "\x1b[33m ( o ) \x1b[0m",
157
+ "\x1b[33m ( | ) \x1b[0m",
158
+ "\x1b[38;5;220m ( 0 ) \x1b[0m",
159
+ "\x1b[33m ( | ) \x1b[0m"
160
+ ];
139
161
 
140
- let count = 3;
162
+ let i = 0;
141
163
  const timer = setInterval(() => {
142
- if (count > 0) {
143
- const countMsg = ` ⏳ Girando en el aire... ${count}s`;
144
- server.send(countMsg);
145
- broadcast(countMsg, server);
146
- count--;
164
+ if (i < 15) { // 3 segundos a 200ms
165
+ const frame = `\r\x1b[A\x1b[K ${asciiCoins[i % asciiCoins.length]} zumbando...`;
166
+ server.send(frame);
167
+ broadcast(frame, server);
168
+ i++;
147
169
  } else {
148
170
  clearInterval(timer);
149
171
  const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
150
- const renderMsg = `\n[${getTime()}] 🎯 ¡CAYÓ LA MONEDA de ${myName}! Es -> \x1b[1m\x1b[33m${result}\x1b[0m\n`;
172
+ const renderMsg = `\r\x1b[A\x1b[K[${getTime()}] 🎯 ¡CAYÓ LA MONEDA de ${myName}! Es -> \x1b[1m\x1b[33m${result}\x1b[0m\n`;
151
173
  server.send(renderMsg);
152
174
  broadcast(renderMsg, server);
153
175
  }
154
- }, 1000);
176
+ }, 200);
155
177
  return;
156
178
  }
157
179
 
@@ -161,12 +183,12 @@ wss.on("connection", (server) => {
161
183
  server.send(introMsg);
162
184
  broadcast(introMsg, server);
163
185
 
164
- const asciiArt = ` _____ _ _ _ _ _____ _ _ _ _
165
- |_ _(_) | (_) | |_ _(_) | | | |
166
- | | _ ___ _ __ __| |_| |_ __ _ | | _ _ __ ___ ___| | | |
167
- | | | |/ _ \\ '_ \\ / _\` | | __/ _\` | | | | | '_ \` _ \\ / _ \\ | | |
168
- | | | | __/ | | | (_| | | || (_| | | | | | | | | | | __/_|_|_|
169
- \\_/ |_|\\___|_| |_|\\__,_|_|\\__\\__,_| \\_/ |_|_| |_| |_|\\___(_|_|_)`;
186
+ const asciiArt = `_____ _ _ _ _ _ _ _
187
+ |_ _(_) ___ _ __ __| (_) |_ __ _| | | |
188
+ | | | |/ _ \ '_ \ / _ | | __/ _ | | | |
189
+ | | | | __/ | | | (_ | | || (_ |_|_|_|
190
+ |_| |_|\___|_| |_|\__,_|_|\__\__,_(_|_|_)
191
+ `;
170
192
 
171
193
  const baseColors = [
172
194
  "\x1b[31m", // Rojo
@@ -238,10 +260,31 @@ wss.on("connection", (server) => {
238
260
  return;
239
261
  }
240
262
 
241
- // Chat
242
- const msg = `\n[${getTime()}] 🌍 [${myEmoji} ${myName}]: ${text}`;
243
- server.send(msg);
244
- broadcast(msg, server);
263
+ // Analizar Menciones con @
264
+ const formattedText = text.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
265
+
266
+ // Notificar con Ring! a los mencionados (si existen)
267
+ const mentions = [...text.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
268
+ for (const m of mentions) {
269
+ if (targetExists(m) && m !== myName && m !== socketData.privateTarget) {
270
+ sendToTarget(m, "\x07"); // ASCII Bell character
271
+ }
272
+ }
273
+
274
+ // Chat Normal o Modo Susurro Fijo
275
+ if (socketData.privateTarget) {
276
+ const target = socketData.privateTarget;
277
+ if (!targetExists(target)) {
278
+ server.send(`❌ ${target} ya no está conectado. Escribe /all para salir del modo privado.`);
279
+ return;
280
+ }
281
+ server.send(`\n[${getTime()}] 🔒 [Private → ${target}]: \x1b[35m${formattedText}\x1b[0m`);
282
+ sendToTarget(target, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${formattedText}\x1b[0m`);
283
+ } else {
284
+ const msg = `\n[${getTime()}] 🌍 [${myEmoji} ${myName}]: ${formattedText}`;
285
+ server.send(msg);
286
+ broadcast(msg, server);
287
+ }
245
288
  });
246
289
 
247
290
  server.on("close", () => {