@pdc-test/chat-io 1.0.0 → 1.0.1

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,6 +25,7 @@ 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;
30
31
 
@@ -42,26 +43,26 @@ process.stdin.on('keypress', (str, key) => {
42
43
  clearTimeout(typingTimeout);
43
44
  typingTimeout = setTimeout(() => {
44
45
  isTyping = false;
45
- }, 2000);
46
+ if (ws.readyState === WebSocket.OPEN) ws.send("/typ_stop");
47
+ }, 1500);
46
48
  }
47
49
  });
48
50
 
49
- // Renderizado de "Escribiendo..." Dinámico en la Barra Inferior
51
+ // Renderizado de "Escribiendo..." en la parte SUPERIOR (En el Prompt)
50
52
  setInterval(() => {
51
53
  if (typingUsers.size > 0) {
52
- readline.clearLine(process.stdout, 0);
53
- readline.cursorTo(process.stdout, 0);
54
-
55
54
  const users = Array.from(typingUsers).join(", ");
56
55
  const suffix = users.includes(",") ? "están escribiendo" : "está escribiendo";
57
56
 
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`);
57
+ // Cambiar el diseño del prompt para mostrar texto gris ANTES del "> "
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
+ } else {
62
+ // Restaurar prompt normal cuando nadie escribe
63
+ rl.setPrompt('> ');
64
+ // No forzamos rl.prompt() contínuamente aquí para evitar pestañeo,
65
+ // solo esperamos que rl.prompt() se dibuje en el input o mensaje
65
66
  }
66
67
  }, 400);
67
68
 
@@ -79,12 +80,25 @@ ws.on('message', (data) => {
79
80
  if (msg.startsWith("/typ_event ")) {
80
81
  const u = msg.replace("/typ_event ", "").trim();
81
82
  typingUsers.add(u);
82
- setTimeout(() => typingUsers.delete(u), 3000); // 3seg de cooldown maximo
83
+ clearTimeout(userTypingTimers[u]);
84
+ userTypingTimers[u] = setTimeout(() => typingUsers.delete(u), 3500);
83
85
  return;
84
86
  }
85
87
 
86
- // Si recibimos texto normal, limpiamos indicadores para que no estorben
88
+ if (msg.startsWith("/typ_stop ")) {
89
+ const u = msg.replace("/typ_stop ", "").trim();
90
+ typingUsers.delete(u);
91
+ clearTimeout(userTypingTimers[u]);
92
+
93
+ if (typingUsers.size === 0) {
94
+ rl.setPrompt('> ');
95
+ }
96
+ return;
97
+ }
98
+
99
+ // Si recibimos texto normal, limpiamos indicadores que ya terminaron
87
100
  typingUsers.clear();
101
+ rl.setPrompt('> ');
88
102
 
89
103
  // Purgamos la línea completa en la consola antes de dibujar la obra de Arte/Chat
90
104
  readline.clearLine(process.stdout, 0);
@@ -96,10 +110,16 @@ ws.on('message', (data) => {
96
110
 
97
111
  // Cuando le damos ENTER
98
112
  rl.on('line', (input) => {
113
+ // Borramos la línea que acabas de teclear para que no se duplique
114
+ // con el mensaje que el servidor nos devolverá en azul
115
+ readline.moveCursor(process.stdout, 0, -1);
116
+ readline.clearLine(process.stdout, 0);
117
+
99
118
  const line = input.trim();
100
119
  if (line) {
101
120
  ws.send(line);
102
- isTyping = false; // Matamos flag al presionar intro
121
+ isTyping = false;
122
+ ws.send("/typ_stop");
103
123
  }
104
124
  rl.prompt(true);
105
125
  });
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.1",
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.1",
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,13 @@ 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
+ |_| |_|\___|_| |_|\__,_|_|\__\__,_(_|_|_)
192
+ `;
170
193
 
171
194
  const baseColors = [
172
195
  "\x1b[31m", // Rojo
@@ -238,10 +261,31 @@ wss.on("connection", (server) => {
238
261
  return;
239
262
  }
240
263
 
241
- // Chat
242
- const msg = `\n[${getTime()}] 🌍 [${myEmoji} ${myName}]: ${text}`;
243
- server.send(msg);
244
- broadcast(msg, server);
264
+ // Analizar Menciones con @
265
+ const formattedText = text.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
266
+
267
+ // Notificar con Ring! a los mencionados (si existen)
268
+ const mentions = [...text.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
269
+ for (const m of mentions) {
270
+ if (targetExists(m) && m !== myName && m !== socketData.privateTarget) {
271
+ sendToTarget(m, "\x07"); // ASCII Bell character
272
+ }
273
+ }
274
+
275
+ // Chat Normal o Modo Susurro Fijo
276
+ if (socketData.privateTarget) {
277
+ const target = socketData.privateTarget;
278
+ if (!targetExists(target)) {
279
+ server.send(`❌ ${target} ya no está conectado. Escribe /all para salir del modo privado.`);
280
+ return;
281
+ }
282
+ server.send(`\n[${getTime()}] 🔒 [Private → ${target}]: \x1b[35m${formattedText}\x1b[0m`);
283
+ sendToTarget(target, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${formattedText}\x1b[0m`);
284
+ } else {
285
+ const msg = `\n[${getTime()}] 🌍 [${myEmoji} ${myName}]: ${formattedText}`;
286
+ server.send(msg);
287
+ broadcast(msg, server);
288
+ }
245
289
  });
246
290
 
247
291
  server.on("close", () => {