@getparallel/parx 0.4.0 → 0.4.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/dist/tui.js CHANGED
@@ -24,6 +24,33 @@ export function bloqueDeTexto(texto, sangria = '') {
24
24
  return out;
25
25
  };
26
26
  }
27
+ export function bloqueDeTabla(filas, o = {}) {
28
+ return (ancho) => {
29
+ const util = anchoUtil(ancho);
30
+ const sangria = ' '.repeat(o.sangria ?? 0);
31
+ const dentro = util - sangria.length;
32
+ const col = Math.min(Math.max(0, ...filas.map(([k]) => anchoDe(k))) + 3, Math.floor(dentro / 2));
33
+ const out = [];
34
+ if (o.titulo)
35
+ out.push(`${m}${c.negrita(o.titulo)}`);
36
+ for (const [k, d] of filas) {
37
+ const resto = dentro - col;
38
+ if (resto < 12) {
39
+ out.push(`${m}${sangria}${c.marca(recortar(k, dentro))}`);
40
+ for (const l of envolver(d, dentro - 2))
41
+ out.push(`${m}${sangria} ${l}`);
42
+ continue;
43
+ }
44
+ envolver(d, resto).forEach((l, i) => out.push(`${m}${sangria}${i === 0 ? c.marca(rellenar(recortar(k, col - 1), col)) : ' '.repeat(col)}${l}`));
45
+ }
46
+ if (o.pie) {
47
+ out.push('');
48
+ for (const l of envolver(o.pie, util))
49
+ out.push(`${m}${c.gris(l)}`);
50
+ }
51
+ return out;
52
+ };
53
+ }
27
54
  export function bloqueDeLinea(linea) {
28
55
  return (ancho) => [`${m}${recortar(linea, anchoUtil(ancho))}`];
29
56
  }
@@ -108,6 +135,26 @@ export function bloqueDeCierre(texto, marca) {
108
135
  return [`${m}${izq}${resto > 0 ? ` ${c.tenue(c.gris((capacidades().unicode ? '─' : '-').repeat(resto)))}` : ''}`];
109
136
  };
110
137
  }
138
+ export function bloqueDeBash(comando) {
139
+ return (ancho) => {
140
+ const util = anchoUtil(ancho);
141
+ return ['', ...envolver(comando, util - 2).map((l, i) => `${m}${i === 0 ? c.aviso(c.negrita('!')) : ' '} ${l}`)];
142
+ };
143
+ }
144
+ export function bloqueDeSalidaDeBash(lineas) {
145
+ return (ancho) => {
146
+ const util = anchoUtil(ancho);
147
+ const raya = c.tenue(c.aviso(capacidades().unicode ? '│' : '|'));
148
+ return lineas.map((l) => `${m}${raya} ${recortar(l.replace(/\t/g, ' '), util - 2)}`);
149
+ };
150
+ }
151
+ export function bloqueDeFinDeBash(bien, texto) {
152
+ return (ancho) => {
153
+ const u = capacidades().unicode;
154
+ const marca = bien ? c.ok(u ? '✓' : 'v') : c.error(u ? '✗' : 'x');
155
+ return [`${m}${c.tenue(c.aviso(u ? '╰' : '`'))} ${marca} ${recortar(c.gris(texto), anchoUtil(ancho) - 4)}`];
156
+ };
157
+ }
111
158
  export function trocearTeclas(datos, estado) {
112
159
  const out = [];
113
160
  let i = 0;
@@ -125,6 +172,20 @@ export function trocearTeclas(datos, estado) {
125
172
  }
126
173
  const ch = datos[i];
127
174
  if (ch === '\x1b') {
175
+ const sgr = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/.exec(datos.slice(i));
176
+ if (sgr) {
177
+ i += sgr[0].length;
178
+ out.push(teclaDeRaton(Number(sgr[1]), Number(sgr[2]), Number(sgr[3]), sgr[4] === 'm'));
179
+ continue;
180
+ }
181
+ if (datos.startsWith('\x1b[M', i) && datos.length >= i + 6) {
182
+ const b = datos.charCodeAt(i + 3) - 32;
183
+ const x = datos.charCodeAt(i + 4) - 32;
184
+ const y = datos.charCodeAt(i + 5) - 32;
185
+ i += 6;
186
+ out.push(teclaDeRaton(b, x, y, (b & 3) === 3 && (b & 64) === 0 && (b & 32) === 0, b & 64 ? 3 : b & 3));
187
+ continue;
188
+ }
128
189
  const r = /^\x1b(?:\[([0-9;?]*)([ -/]*[@-~])|O([@-~])|([\s\S]))?/.exec(datos.slice(i));
129
190
  const seq = r[0];
130
191
  i += Math.max(1, seq.length);
@@ -147,10 +208,16 @@ export function trocearTeclas(datos, estado) {
147
208
  out.push({ t: 'abajo' });
148
209
  else if (fin === 'H' || (fin === '~' && (params === '1' || params === '7')))
149
210
  out.push({ t: 'inicio' });
211
+ else if ((fin === 'F' && /;5$/.test(params)) || (fin === '~' && /^[48];5$/.test(params)))
212
+ out.push({ t: 'fondo' });
150
213
  else if (fin === 'F' || (fin === '~' && (params === '4' || params === '8')))
151
214
  out.push({ t: 'fin' });
152
215
  else if (fin === '~' && params === '3')
153
216
  out.push({ t: 'suprimir' });
217
+ else if (fin === '~' && params === '5')
218
+ out.push({ t: 'pagina_arriba' });
219
+ else if (fin === '~' && params === '6')
220
+ out.push({ t: 'pagina_abajo' });
154
221
  else if (fin === 'Z')
155
222
  out.push({ t: 'cambiar' });
156
223
  else if (fin === 'I' && params === '')
@@ -194,6 +261,8 @@ export function trocearTeclas(datos, estado) {
194
261
  out.push({ t: 'borrar_palabra' });
195
262
  else if (ch === '\x0c')
196
263
  out.push({ t: 'limpiar' });
264
+ else if (ch === '\x1a')
265
+ out.push({ t: 'suspender' });
197
266
  else if (ch === '\x10')
198
267
  out.push({ t: 'arriba' });
199
268
  else if (ch === '\x0e')
@@ -210,8 +279,17 @@ export function trocearTeclas(datos, estado) {
210
279
  }
211
280
  return out;
212
281
  }
213
- const CON_VALOR = new Set(['mode', 'model', 'tips', 'notify', 'teach']);
282
+ function teclaDeRaton(b, x, y, soltar, botonX10) {
283
+ const rueda = b & 64 ? ((b & 1) === 0 ? -1 : 1) : 0;
284
+ const mover = Boolean(b & 32);
285
+ const boton = botonX10 ?? (rueda ? 3 : b & 3);
286
+ return { t: 'raton', boton, x, y, soltar, mover, rueda: rueda };
287
+ }
288
+ export const RATON_SI = '\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h';
289
+ export const RATON_NO = '\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l';
290
+ const CON_VALOR = new Set(['mode', 'model', 'tips', 'notify', 'teach', 'mouse']);
214
291
  const CON_TEXTO = new Set(['folder']);
292
+ const SE_MANDAN_SOLOS = new Set(['teach', 'notify', 'tips', 'folder', 'mouse']);
215
293
  const MODOS_DEL_MENU = ['ask', 'auto', 'read', 'bypass'];
216
294
  export function opcionesDelMenu(valor) {
217
295
  if (!valor.startsWith('/') || valor.includes('\n'))
@@ -226,7 +304,8 @@ export function opcionesDelMenu(valor) {
226
304
  if (!casa)
227
305
  continue;
228
306
  const conAlgo = CON_VALOR.has(cmd) || CON_TEXTO.has(cmd);
229
- out.push({ nombre: `/${cmd}`, descripcion, completar: `/${cmd}${conAlgo ? ' ' : ''}`, enviar: !conAlgo });
307
+ const entero = q === cmd || ALIAS_COMANDO[q] === cmd;
308
+ out.push({ nombre: `/${cmd}`, descripcion, completar: `/${cmd}${conAlgo ? ' ' : ''}`, enviar: !conAlgo || (entero && SE_MANDAN_SOLOS.has(cmd)) });
230
309
  }
231
310
  return out;
232
311
  }
@@ -242,31 +321,51 @@ export function opcionesDelMenu(valor) {
242
321
  : cmd === 'model'
243
322
  ? ORDEN_DE_MODELOS.map((k) => [k, `${MODELOS[k].nombre} ${MODELOS[k].version}: ${MODELOS[k].descripcion}`])
244
323
  : [
245
- ['on', cmd === 'teach' ? 'Explain each step as it codes' : cmd === 'notify' ? 'Notify me when it finishes or needs me' : 'Show them when parx opens'],
246
- ['off', cmd === 'teach' ? 'Back to normal' : cmd === 'notify' ? 'No desktop notifications' : 'Don’t show them again'],
324
+ ['on', cmd === 'teach' ? 'Explain each step as it codes' : cmd === 'notify' ? 'Notify me when it finishes or needs me' : cmd === 'mouse' ? 'Click options, scroll with the wheel' : 'Show them when parx opens'],
325
+ ['off', cmd === 'teach' ? 'Back to normal' : cmd === 'notify' ? 'No desktop notifications' : cmd === 'mouse' ? 'The terminal selects and scrolls as usual' : 'Don’t show them again'],
247
326
  ];
248
327
  return valores
249
328
  .filter(([v]) => v.startsWith(q))
250
329
  .map(([v, d]) => ({ nombre: v, descripcion: d, completar: `${base}${v}`, enviar: true, ...(cmd === 'mode' && v === 'bypass' ? { peligro: true } : {}) }));
251
330
  }
252
331
  export function componerBarra(e, util) {
332
+ return componerBarraConZonas(e, util).texto;
333
+ }
334
+ export function componerBarraConZonas(e, util) {
335
+ const u = capacidades().unicode;
253
336
  const punto = c.gris(sep());
254
- const modo = e.peligro ? c.aviso(c.negrita(`${capacidades().unicode ? '▲' : '!'} ${e.modo}`)) : c.gris(e.modo);
337
+ const modo = e.peligro ? c.aviso(c.negrita(`${u ? '▲' : '!'} ${e.modo}`)) : c.gris(e.modo);
255
338
  const extra = e.extra ? `${punto}${c.marca(e.extra)}` : '';
339
+ const n = e.esperando ?? 0;
340
+ const espera = n ? c.aviso(`${u ? '●' : '*'} ${tt('bandeja.esperando', { n })}`) : '';
341
+ const esperaCorta = n ? c.aviso(`${u ? '●' : '*'} ${n}`) : '';
342
+ const conEspera = (base, e2) => (e2 ? [...base, { t: punto }, { t: e2, id: 'bandeja' }] : base);
343
+ const m = { t: modo, id: 'modo' };
256
344
  const opciones = [
257
- [`${modo}${punto}${c.gris(e.modelo)}${extra}`, e.saldo],
258
- [`${modo}${punto}${c.gris(e.modelo)}${extra}`, e.saldoCorto ?? e.saldo],
259
- [`${modo}${punto}${c.gris(e.modelo)}`, e.saldoCorto ?? e.saldo],
260
- [modo, e.saldoCorto ?? e.saldo],
345
+ [conEspera([m, { t: `${punto}${c.gris(e.modelo)}${extra}` }], espera), e.saldo],
346
+ [conEspera([m, { t: `${punto}${c.gris(e.modelo)}${extra}` }], espera), e.saldoCorto ?? e.saldo],
347
+ [conEspera([m, { t: `${punto}${c.gris(e.modelo)}` }], esperaCorta), e.saldoCorto ?? e.saldo],
348
+ [conEspera([m], esperaCorta), e.saldoCorto ?? e.saldo],
349
+ [[m], e.saldoCorto ?? e.saldo],
261
350
  ];
262
- for (const [izq, der] of opciones) {
351
+ for (const [trozos, der] of opciones) {
352
+ const izq = trozos.map((x) => x.t).join('');
263
353
  const hueco = util - anchoDe(izq) - anchoDe(der);
264
- if (hueco >= 2)
265
- return `${izq}${' '.repeat(hueco)}${c.gris(der)}`;
354
+ if (hueco < 2)
355
+ continue;
356
+ const zonas = [];
357
+ let col = 0;
358
+ for (const x of trozos) {
359
+ const w = anchoDe(x.t);
360
+ if (x.id)
361
+ zonas.push({ desde: col, hasta: col + w, id: x.id });
362
+ col += w;
363
+ }
364
+ return { texto: `${izq}${' '.repeat(hueco)}${c.gris(der)}`, zonas };
266
365
  }
267
366
  const der = e.saldoCorto ?? '';
268
367
  const izq = recortar(modo, Math.max(1, util - anchoDe(der) - 2));
269
- return rellenar(`${izq}${' '.repeat(Math.max(0, util - anchoDe(izq) - anchoDe(der)))}${c.gris(der)}`, util);
368
+ return { texto: rellenar(`${izq}${' '.repeat(Math.max(0, util - anchoDe(izq) - anchoDe(der)))}${c.gris(der)}`, util), zonas: [{ desde: 0, hasta: anchoDe(izq), id: 'modo' }] };
270
369
  }
271
370
  export function duracion(ms) {
272
371
  const s = Math.max(0, Math.floor(ms / 1000));
@@ -308,11 +407,16 @@ export function textoDeTrabajo(w, ahora) {
308
407
  return { texto: w.texto, anim: w.anim };
309
408
  return { texto: fraseDeEspera(ahora - w.esperaDesde, w.semilla), anim: 'ruta' };
310
409
  }
311
- export function filaDeTrabajo(w, ahora) {
410
+ export function filaDeTrabajo(w, ahora, ancho = Infinity) {
312
411
  const { texto, anim } = textoDeTrabajo(w, ahora);
313
412
  const tiempo = duracion(ahora - w.desde);
314
413
  const gasto = w.acciones ? `${sep()}${acciones(w.acciones)}` : '';
315
- return `${animacion(anim, w.paso)} ${texto}${c.gris(`${sep()}${tiempo}${gasto}`)}`;
414
+ for (const cola of [`${sep()}${tiempo}${gasto}`, `${sep()}${tiempo}${w.acciones ? `${sep()}${w.acciones.toLocaleString('en-US')}` : ''}`, `${sep()}${tiempo}`]) {
415
+ const fila = `${animacion(anim, w.paso)} ${texto}${c.gris(cola)}`;
416
+ if (anchoDe(fila) <= ancho)
417
+ return fila;
418
+ }
419
+ return recortar(`${animacion(anim, w.paso)} ${texto}`, ancho);
316
420
  }
317
421
  export function filaDeCarril(k, util, ahora) {
318
422
  const fin = k.hasta ?? ahora;
@@ -333,8 +437,11 @@ export function filaDeCarril(k, util, ahora) {
333
437
  }
334
438
  const anchoTarea = Math.max(8, Math.floor(util * 0.32));
335
439
  const tarea = recortar(k.tarea.replace(/\s+/g, ' '), anchoTarea);
336
- const izq = `${numero} ${señal} ${c.negrita(tarea)}${c.gris(sep())}${c.gris(texto)}`;
337
- return izquierdaYDerecha(recortar(izq, util - (anchoDe(derecha) + 2 <= util / 2 ? anchoDe(derecha) + 2 : 0)), derecha, util);
440
+ const sitio = util - (anchoDe(derecha) + 2 <= util / 2 ? anchoDe(derecha) + 2 : 0);
441
+ const base = `${numero} ${señal} ${c.negrita(tarea)}`;
442
+ const libre = sitio - anchoDe(base) - anchoDe(sep());
443
+ const izq = libre >= 8 ? `${base}${c.gris(sep())}${c.gris(recortar(texto, libre))}` : base;
444
+ return izquierdaYDerecha(recortar(izq, sitio), derecha, util);
338
445
  }
339
446
  function izquierdaYDerecha(izq, der, util) {
340
447
  if (anchoDe(izq) + anchoDe(der) + 2 <= util)
@@ -367,77 +474,163 @@ export function ventanaDeEntrada(unidades, cursor, ancho) {
367
474
  export function cabePar(ancho) {
368
475
  return ancho >= 56;
369
476
  }
477
+ export function esBash(unidades) {
478
+ return unidades[0] === '!';
479
+ }
480
+ export function boton(etiqueta, numero, o = {}) {
481
+ const u = capacidades().unicode;
482
+ const n = numero === null ? '' : `${numero} `;
483
+ if (o.elegido)
484
+ return c.boton(`[${u ? '▸' : '>'}${n}${etiqueta} ]`, o.peligro);
485
+ const pinta = o.peligro ? c.peligro : (x) => x;
486
+ if (o.sobre)
487
+ return `${c.claro('[')} ${c.marca(`${n}${etiqueta}`)} ${c.claro(']')}`;
488
+ return `${c.gris('[')} ${numero === null ? '' : c.claro(n)}${pinta(etiqueta)} ${c.gris(']')}`;
489
+ }
490
+ function filaDeBotones(etiquetas, p, sobre, ancho) {
491
+ const partes = etiquetas.map((e, i) => boton(e, i + 1, { elegido: i === p.elegida, sobre: sobre === `opcion:${i}`, peligro: p.peligro || p.notas?.[i]?.peligro }));
492
+ const total = partes.reduce((n, x) => n + anchoDe(x), 0) + 2 * (partes.length - 1);
493
+ if (total > ancho)
494
+ return null;
495
+ const zonas = [];
496
+ let col = 0;
497
+ partes.forEach((x, i) => {
498
+ zonas.push({ desde: col, hasta: col + anchoDe(x), id: `opcion:${i}` });
499
+ col += anchoDe(x) + 2;
500
+ });
501
+ return { texto: partes.join(' '), zonas };
502
+ }
370
503
  export function componerCarril(d, ancho, alto) {
371
504
  const util = anchoUtil(ancho);
372
505
  const conPar = cabePar(ancho);
373
506
  const huecoPar = conPar ? ANCHO_DE_PAR + 2 : 0;
374
- const signoW = 3;
507
+ const u = capacidades().unicode;
508
+ const bash = !d.pregunta && esBash(d.unidades);
509
+ const signoW = bash ? 2 : 3;
510
+ const holgado = alto >= 14;
511
+ const medio = alto >= 9;
512
+ const vacia = `${m}${' '.repeat(util)}`;
375
513
  let filaEscribir;
376
514
  let cursor = null;
377
515
  const anchoEscribir = util - (conPar ? huecoPar + 1 : 0);
378
- if (d.pregunta) {
379
- const p = d.pregunta;
380
- const pinta = (etiquetas) => etiquetas
381
- .map((e, i) => {
382
- const txt = `${i + 1} ${e}`;
383
- return i === p.elegida ? `${c.claro(capacidades().unicode ? '▸' : '>')} ${c.negrita(txt)}` : ` ${c.gris(txt)}`;
384
- })
385
- .join(' ');
386
- let opciones = pinta(p.opciones);
387
- if (anchoDe(opciones) > anchoEscribir - signoW)
388
- opciones = pinta(p.cortas);
389
- filaEscribir = rellenar(`${(p.peligro ? c.peligro : c.aviso)(signo())} ${opciones}`, anchoEscribir);
390
- }
391
- else {
516
+ {
392
517
  const libre = Math.max(4, anchoEscribir - signoW);
393
- const color = d.trabajo ? c.gris : c.claro;
394
- if (!d.unidades.length) {
395
- filaEscribir = rellenar(`${color(signo())} ${c.gris(recortar(d.ejemplo, libre))}`, anchoEscribir);
518
+ const color = d.trabajo || d.pregunta ? c.gris : c.claro;
519
+ const marca = bash ? c.aviso(c.negrita('!')) : color(signo());
520
+ const unidades = bash ? d.unidades.slice(1) : d.unidades;
521
+ const enCursor = bash ? Math.max(0, d.cursor - 1) : d.cursor;
522
+ if (!unidades.length) {
523
+ const ejemplo = bash ? tt('bash.ejemplo') : d.pregunta ? '' : d.ejemplo;
524
+ filaEscribir = rellenar(`${marca} ${c.gris(recortar(ejemplo, libre))}`, anchoEscribir);
396
525
  cursor = { fila: 0, col: MARGEN + signoW };
397
526
  }
398
527
  else {
399
- const v = ventanaDeEntrada(d.unidades, d.cursor, libre);
400
- filaEscribir = rellenar(`${color(signo())} ${v.texto}`, anchoEscribir);
528
+ const v = ventanaDeEntrada(unidades, enCursor, libre);
529
+ filaEscribir = rellenar(`${marca} ${d.pregunta ? c.gris(v.texto) : v.texto}`, anchoEscribir);
401
530
  cursor = { fila: 0, col: MARGEN + signoW + v.col };
402
531
  }
532
+ if (d.pregunta)
533
+ cursor = null;
403
534
  }
404
535
  if (conPar)
405
536
  filaEscribir += ` ${cabezaDePar()} `;
406
- const tinta = (t) => c.violeta(t);
537
+ const tinta = (t) => (bash ? c.aviso(t) : c.violeta(t));
407
538
  const linea = conPar ? `${tinta(carril(util - ANCHO_DE_PAR - 2))}${caraDePar(d.gesto)}${tinta(carril(2))}` : tinta(carril(util));
408
539
  let barra;
540
+ let zonasBarra = [];
409
541
  const enCarriles = (d.carriles ?? []).filter((k) => k.estado === 'trabaja').length;
410
542
  if (d.trabajo && !d.pregunta && d.carriles?.length) {
411
543
  const gasto = d.carriles.reduce((n, k) => n + (k.trabajo.acciones ?? 0), 0);
412
- const izq = `${animacion('tren', d.trabajo.paso)} ${tt('carriles.en_marcha', { n: enCarriles, total: d.carriles.length })}${c.gris(`${sep()}${duracion(d.ahora - d.trabajo.desde)}${gasto ? `${sep()}${acciones(gasto)}` : ''}`)}`;
413
- barra = izquierdaYDerecha(recortar(izq, util), c.tenue(c.gris(d.aviso ?? tt('tui.parar'))), util);
544
+ const base = `${animacion('tren', d.trabajo.paso)} ${tt('carriles.en_marcha', { n: enCarriles, total: d.carriles.length })}`;
545
+ const tiempo = `${sep()}${duracion(d.ahora - d.trabajo.desde)}`;
546
+ const izq = [`${base}${c.gris(`${tiempo}${gasto ? `${sep()}${acciones(gasto)}` : ''}`)}`, `${base}${c.gris(tiempo)}`, base].find((x) => anchoDe(x) <= util) ?? recortar(base, util);
547
+ barra = izquierdaYDerecha(izq, c.tenue(c.gris(d.aviso ?? tt('tui.parar'))), util);
414
548
  }
415
549
  else if (d.trabajo && !d.pregunta) {
416
- barra = izquierdaYDerecha(recortar(filaDeTrabajo(d.trabajo, d.ahora), util), c.tenue(c.gris(d.aviso ?? tt('tui.parar'))), util);
550
+ barra = izquierdaYDerecha(filaDeTrabajo(d.trabajo, d.ahora, util), c.tenue(c.gris(d.aviso ?? tt('tui.parar'))), util);
417
551
  }
418
552
  else if (d.aviso) {
419
553
  barra = izquierdaYDerecha(c.aviso(recortar(d.aviso, util)), '', util);
420
554
  }
555
+ else if (bash) {
556
+ const etiqueta = c.aviso(c.negrita(tt('bash.etiqueta')));
557
+ const largo = `${etiqueta}${c.gris(`${sep()}${tt('bash.intro')}`)}`;
558
+ barra = izquierdaYDerecha(anchoDe(largo) + anchoDe(d.estado.saldoCorto ?? '') + 2 <= util ? largo : etiqueta, c.gris(d.estado.saldoCorto ?? ''), util);
559
+ }
421
560
  else {
422
- barra = componerBarra(d.estado, util);
561
+ const b = componerBarraConZonas(d.estado, util);
562
+ barra = b.texto;
563
+ zonasBarra = b.zonas;
423
564
  }
424
- const base = [filaEscribir, linea, barra].map((f) => `${m}${f}`);
425
- let filas = alto >= 3 ? base : alto === 2 ? [base[0], base[2]] : [base[0]];
426
- const carriles = (d.carriles ?? []).slice(0, Math.max(0, alto - filas.length - 2));
565
+ const filas = [filaEscribir, linea, ...(holgado ? [''] : []), barra].map((f) => (f ? `${m}${f}` : vacia));
566
+ const zonas = [];
567
+ const filaBarra = filas.length - 1;
568
+ for (const z of zonasBarra)
569
+ zonas.push({ fila: filaBarra, desde: MARGEN + z.desde, hasta: MARGEN + z.hasta, id: z.id });
570
+ let abajo = alto >= 3 ? filas : alto === 2 ? [filas[0], filas[filas.length - 1]] : [filas[0]];
571
+ if (abajo !== filas)
572
+ zonas.length = 0;
573
+ const carriles = (d.carriles ?? []).slice(0, Math.max(0, alto - abajo.length - 2));
427
574
  const filasDeCarriles = carriles.map((k) => `${m}${rellenar(filaDeCarril(k, util, d.ahora), util)}`);
428
- filas = [...filasDeCarriles, ...filas];
429
- if (cursor)
430
- cursor = { fila: cursor.fila + filasDeCarriles.length, col: cursor.col };
431
- const hueco = Math.max(0, alto - filas.length);
432
- let encima = [];
433
- if (d.pregunta) {
575
+ let arriba = [...filasDeCarriles];
576
+ const zonasPregunta = [];
577
+ if (d.pregunta && alto >= 3) {
434
578
  const p = d.pregunta;
435
- const marca = p.peligro ? c.peligro(capacidades().unicode ? '▲' : '!') : c.aviso('?');
436
- encima = envolver(p.texto, util - 2).map((l, i) => `${m}${rellenar(`${i === 0 ? marca : ' '} ${c.negrita(p.peligro ? c.peligro(l) : l)}`, util)}`);
437
- if (encima.length > hueco)
438
- encima = [`${m}${rellenar(`${marca} ${c.negrita(p.texto)}`, util)}`];
579
+ const marca = p.peligro ? c.peligro(u ? '▲' : '!') : c.aviso('?');
580
+ const bloque = [];
581
+ const sitio = Math.max(1, alto - abajo.length - arriba.length - 2);
582
+ const titulo = envolver(p.texto, util - 2);
583
+ bloque.push(...titulo.map((l, i) => `${m}${rellenar(`${i === 0 ? marca : ' '} ${c.negrita(p.peligro ? c.peligro(l) : l)}`, util)}`));
584
+ if (p.detalle)
585
+ bloque.push(...envolver(p.detalle, util - 2).slice(0, 3).map((l) => `${m} ${rellenar(c.gris(l), util - 2)}`));
586
+ if (holgado)
587
+ bloque.push(vacia);
588
+ const enFila = !p.vertical ? (filaDeBotones(p.opciones, p, d.sobre, util - 2) ?? filaDeBotones(p.cortas, p, d.sobre, util - 2)) : null;
589
+ const filasBotones = [];
590
+ const zonasBotones = [];
591
+ if (enFila) {
592
+ filasBotones.push(`${m} ${rellenar(enFila.texto, util - 2)}`);
593
+ for (const z of enFila.zonas)
594
+ zonasBotones.push({ fila: 0, desde: MARGEN + 2 + z.desde, hasta: MARGEN + 2 + z.hasta, id: z.id });
595
+ }
596
+ else {
597
+ const lista = filasDeLista(p, d.sobre, util, Math.max(1, sitio - bloque.length - (holgado ? 1 : 0)));
598
+ filasBotones.push(...lista.filas);
599
+ zonasBotones.push(...lista.zonas);
600
+ }
601
+ if (holgado)
602
+ filasBotones.push(vacia);
603
+ let todo = [...bloque, ...filasBotones];
604
+ let inicioBotones = bloque.length;
605
+ if (todo.length > sitio) {
606
+ const una = `${m}${rellenar(`${marca} ${c.negrita(recortar(p.texto, util - 2))}`, util)}`;
607
+ todo = [una, ...filasBotones.filter((x) => x !== vacia)];
608
+ inicioBotones = 1;
609
+ }
610
+ for (const z of zonasBotones)
611
+ zonasPregunta.push({ ...z, fila: inicioBotones + z.fila });
612
+ arriba = [...todo, ...arriba];
439
613
  }
440
- else if (d.menu && d.menu.opciones.length) {
614
+ let separacion = medio || d.fondo ? vacia : null;
615
+ const zonaFondo = [];
616
+ if (separacion !== null && d.fondo) {
617
+ const texto = `${u ? '↓' : 'v'} ${tt('tui.al_fondo')}${d.fondo.nuevas ? `${sep()}${tt('tui.nuevas', { n: d.fondo.nuevas.toLocaleString('en-US') })}` : ''}`;
618
+ const b = d.sobre === 'fondo' ? c.boton(` ${texto} `) : boton(texto, null);
619
+ separacion = `${m}${rellenar(b, util)}`;
620
+ zonaFondo.push({ fila: 0, desde: MARGEN, hasta: MARGEN + anchoDe(b), id: 'fondo' });
621
+ }
622
+ const cabeza = separacion === null ? [] : [separacion];
623
+ const todas = [...cabeza, ...arriba, ...abajo];
624
+ const desplazar = cabeza.length + arriba.length;
625
+ for (const z of zonas)
626
+ z.fila += desplazar;
627
+ const zonasFinales = [...zonaFondo, ...zonasPregunta.map((z) => ({ ...z, fila: z.fila + cabeza.length })), ...zonas];
628
+ if (cursor)
629
+ cursor = { fila: cursor.fila + desplazar, col: cursor.col };
630
+ const hueco = Math.max(0, alto - todas.length);
631
+ let encima = [];
632
+ const zonasEncima = [];
633
+ if (!d.pregunta && d.menu && d.menu.opciones.length) {
441
634
  const ops = d.menu.opciones;
442
635
  const visibles = Math.min(6, ops.length, hueco);
443
636
  if (visibles > 0) {
@@ -446,20 +639,58 @@ export function componerCarril(d, ancho, alto) {
446
639
  encima = ops.slice(ini, ini + visibles).map((o, k) => {
447
640
  const i = ini + k;
448
641
  const sel = i === d.menu.elegida;
449
- const marca = sel ? (o.peligro ? c.peligro : c.claro)(capacidades().unicode ? '▌' : '>') : ' ';
642
+ const sobre = d.sobre === `menu:${i}`;
643
+ zonasEncima.push({ fila: k, desde: MARGEN, hasta: MARGEN + util, id: `menu:${i}` });
644
+ const marca = sel ? (o.peligro ? c.peligro : c.claro)(u ? '▌' : '>') : ' ';
450
645
  const resto = Math.max(0, util - 2 - anchoNombre);
451
646
  const desc = recortar(o.descripcion, resto);
452
647
  if (o.peligro) {
453
- const nombre = rellenar(recortar(`${capacidades().unicode ? '▲' : '!'} ${o.nombre}`, anchoNombre - 1), anchoNombre);
454
- return `${m}${rellenar(`${marca} ${sel ? c.negrita(c.peligro(nombre)) : c.peligro(nombre)}${sel ? c.peligro(desc) : c.tenue(c.peligro(desc))}`, util)}`;
648
+ const nombre = rellenar(recortar(`${u ? '▲' : '!'} ${o.nombre}`, anchoNombre - 1), anchoNombre);
649
+ return `${m}${rellenar(`${marca} ${sel || sobre ? c.negrita(c.peligro(nombre)) : c.peligro(nombre)}${sel ? c.peligro(desc) : c.tenue(c.peligro(desc))}`, util)}`;
455
650
  }
456
651
  const nombre = rellenar(recortar(o.nombre, anchoNombre - 1), anchoNombre);
457
- return `${m}${rellenar(`${marca} ${sel ? c.negrita(c.marca(nombre)) : c.gris(nombre)}${sel ? desc : c.tenue(c.gris(desc))}`, util)}`;
652
+ return `${m}${rellenar(`${marca} ${sel ? c.negrita(c.marca(nombre)) : sobre ? c.marca(nombre) : c.gris(nombre)}${sel || sobre ? desc : c.tenue(c.gris(desc))}`, util)}`;
458
653
  });
459
654
  }
460
655
  }
461
- encima = encima.slice(Math.max(0, encima.length - hueco));
462
- return { encima, carril: filas, cursor: cursor && filas.length ? cursor : null };
656
+ const fuera = Math.max(0, encima.length - hueco);
657
+ encima = encima.slice(fuera);
658
+ return {
659
+ encima,
660
+ carril: todas,
661
+ cursor: cursor && todas.length ? cursor : null,
662
+ zonas: zonasFinales,
663
+ zonasEncima: zonasEncima.filter((z) => z.fila >= fuera).map((z) => ({ ...z, fila: z.fila - fuera })),
664
+ };
665
+ }
666
+ function filasDeLista(p, sobre, util, max) {
667
+ const u = capacidades().unicode;
668
+ const n = p.opciones.length;
669
+ const visibles = Math.max(1, Math.min(n, max, 8));
670
+ const ini = Math.max(0, Math.min(p.elegida - visibles + 1, n - visibles));
671
+ const filas = [];
672
+ const zonas = [];
673
+ const recom = anchoDe(tt('bandeja.recomendada')) + 2;
674
+ const anchoEtiqueta = Math.max(...p.opciones.map((o, i) => anchoDe(o) + (p.notas?.[i]?.recomendada ? recom : 0))) + 2;
675
+ const conNotas = anchoEtiqueta <= Math.floor(util * 0.65);
676
+ for (let k = 0; k < visibles; k++) {
677
+ const i = ini + k;
678
+ const sel = i === p.elegida;
679
+ const enc = sobre === `opcion:${i}`;
680
+ const nota = p.notas?.[i];
681
+ const peligro = p.peligro || nota?.peligro;
682
+ const marca = sel ? (peligro ? c.peligro : c.claro)(u ? '▌' : '>') : ' ';
683
+ const num = i < 9 ? `${i + 1}` : ' ';
684
+ const recomendada = nota?.recomendada ? ` ${c.marca(tt('bandeja.recomendada'))}` : '';
685
+ const etiqueta = sel ? c.negrita(peligro ? c.peligro(p.opciones[i]) : p.opciones[i]) : enc ? c.marca(p.opciones[i]) : peligro ? c.peligro(p.opciones[i]) : p.opciones[i];
686
+ const cabeza = `${marca} ${c.claro(num)} `;
687
+ const base = conNotas ? `${cabeza}${rellenar(`${etiqueta}${recomendada}`, anchoEtiqueta)}` : `${cabeza}${recortar(`${etiqueta}${recomendada}`, Math.max(4, util - anchoDe(cabeza)))}`;
688
+ const resto = util - anchoDe(base);
689
+ const extra = conNotas && nota?.nota && resto >= 6 ? c.gris(recortar(nota.nota, resto)) : '';
690
+ filas.push(`${m}${rellenar(`${base}${extra}`, util)}`);
691
+ zonas.push({ fila: k, desde: MARGEN, hasta: MARGEN + util, id: `opcion:${i}` });
692
+ }
693
+ return { filas, zonas };
463
694
  }
464
695
  export function opcionesDePregunta(tipo) {
465
696
  switch (tipo) {
@@ -495,20 +726,37 @@ export function rutaDeImagenPegada(texto) {
495
726
  v = decodeURIComponent(v.slice(7));
496
727
  return IMAGEN.test(v) && /^(\/|~|[A-Za-z]:\\|\.)/.test(v) ? v : null;
497
728
  }
729
+ function sinBlancoDoble(lineas, previaEnBlanco) {
730
+ let i = 0;
731
+ if (previaEnBlanco)
732
+ while (i < lineas.length && lineas[i].trim() === '')
733
+ i++;
734
+ return i ? lineas.slice(i) : lineas;
735
+ }
498
736
  export class Superficie {
499
737
  o;
500
738
  bloques = [];
501
739
  lineas = [];
740
+ volcadas = 0;
741
+ subidas = 0;
742
+ vistaSucia = false;
502
743
  anchoCompuesto = 0;
744
+ ultimoAlto = 0;
745
+ ultimoTrasBlanco = false;
503
746
  encimaPintadas = 0;
504
747
  carrilPintadas = 0;
505
748
  activa = false;
506
749
  oyente = null;
507
750
  alRedimensionar = null;
751
+ alReanudar = null;
508
752
  relojResize = null;
509
753
  relojAnim = null;
510
754
  relojAviso = null;
511
755
  pegando = { pegando: false };
756
+ raton;
757
+ zonas = [];
758
+ sobre = null;
759
+ pulsado = null;
512
760
  unidades = [];
513
761
  cursor = 0;
514
762
  pegados = [];
@@ -518,7 +766,7 @@ export class Superficie {
518
766
  borrador = [];
519
767
  menuCerradoPara = null;
520
768
  menuElegida = 0;
521
- cola = null;
769
+ cola = [];
522
770
  ctrlC = 0;
523
771
  ejemplo = null;
524
772
  resolverLectura = null;
@@ -535,6 +783,7 @@ export class Superficie {
535
783
  peligroPintado = false;
536
784
  constructor(o) {
537
785
  this.o = o;
786
+ this.raton = o.raton !== false;
538
787
  }
539
788
  get ancho() {
540
789
  return Math.max(20, this.o.salida.columns || 80);
@@ -545,6 +794,9 @@ export class Superficie {
545
794
  get enfocada() {
546
795
  return this.foco;
547
796
  }
797
+ get conRaton() {
798
+ return this.raton;
799
+ }
548
800
  escribir(s) {
549
801
  this.o.salida.write(s);
550
802
  }
@@ -553,6 +805,7 @@ export class Superficie {
553
805
  const ops = this.menuCerradoPara === valor ? null : opcionesDelMenu(valor);
554
806
  if (ops && this.menuElegida >= ops.length)
555
807
  this.menuElegida = Math.max(0, ops.length - 1);
808
+ const pendientes = this.lineas.length - this.volcadas;
556
809
  return {
557
810
  unidades: this.unidades.map((u) => this.vistaDeUnidad(u)),
558
811
  cursor: this.cursor,
@@ -565,6 +818,8 @@ export class Superficie {
565
818
  carriles: this.carriles,
566
819
  gesto: this.pregunta ? 'pregunta' : this.trabajo || this.carriles.some((k) => k.estado === 'trabaja') ? (((this.trabajo?.paso ?? this.carriles[0]?.trabajo.paso ?? 0) >> 1) % 4) : this.gesto,
567
820
  ahora: Date.now(),
821
+ sobre: this.raton ? this.sobre : null,
822
+ fondo: this.subidas > 0 || pendientes > 0 ? { nuevas: pendientes } : null,
568
823
  };
569
824
  }
570
825
  vistaDeUnidad(u) {
@@ -584,14 +839,31 @@ export class Superficie {
584
839
  return this.alto - n + 1;
585
840
  }
586
841
  desplazadas(altoCarril) {
587
- return Math.max(0, this.lineas.length - (this.alto - altoCarril));
842
+ return Math.max(0, this.volcadas - (this.alto - altoCarril));
588
843
  }
589
844
  filaTrasLaConversacion(altoCarril) {
590
- return Math.min(this.lineas.length, Math.max(0, this.alto - altoCarril)) + 1;
845
+ return Math.min(this.volcadas, Math.max(0, this.alto - altoCarril)) + 1;
591
846
  }
592
847
  lineaDeFila(fila, altoCarril) {
593
- const indice = this.desplazadas(altoCarril) + (fila - 1);
594
- return indice >= 0 ? (this.lineas[indice] ?? '') : '';
848
+ const indice = this.desplazadas(altoCarril) - this.subidas + (fila - 1);
849
+ return indice >= 0 && indice < this.volcadas ? (this.lineas[indice] ?? '') : '';
850
+ }
851
+ componerTodo() {
852
+ const out = [];
853
+ for (const b of this.bloques) {
854
+ this.ultimoTrasBlanco = out.length === 0 || out[out.length - 1].trim() === '';
855
+ const l = sinBlancoDoble(b(this.ancho), this.ultimoTrasBlanco);
856
+ this.ultimoAlto = l.length;
857
+ out.push(...l);
858
+ }
859
+ this.lineas = out;
860
+ }
861
+ recortarMemoria() {
862
+ const sobran = this.lineas.length - MAX_LINEAS;
863
+ if (sobran <= 0)
864
+ return;
865
+ this.lineas.splice(0, sobran);
866
+ this.volcadas = Math.max(0, this.volcadas - sobran);
595
867
  }
596
868
  pintar() {
597
869
  if (!this.activa)
@@ -607,21 +879,34 @@ export class Superficie {
607
879
  const k = componerCarril(this.datos(), this.ancho, this.alto);
608
880
  const n = k.carril.length;
609
881
  let s = '\x1b[?2026h\x1b[?25l';
610
- if (this.carrilPintadas && n > this.carrilPintadas) {
611
- s += this.destapar();
612
- const ocupadas = Math.min(this.lineas.length, Math.max(0, this.alto - this.carrilPintadas));
613
- const subir = Math.max(0, ocupadas - Math.max(0, this.alto - n));
614
- if (subir)
615
- s += `\x1b[${this.alto};1H${'\r\n'.repeat(subir)}`;
882
+ const subido = this.subidas > 0 || this.volcadas < this.lineas.length;
883
+ if (this.carrilPintadas && n !== this.carrilPintadas && subido) {
884
+ this.vistaSucia = true;
885
+ this.encimaPintadas = 0;
616
886
  }
617
- if (this.carrilPintadas && n < this.carrilPintadas) {
618
- s += this.destapar();
619
- const topNuevo = this.filaDelCarril(n);
620
- for (let fila = 1; fila < topNuevo; fila++)
621
- s += `\x1b[${fila};1H\x1b[2K${this.lineaDeFila(fila, n)}`;
887
+ else {
888
+ if (this.carrilPintadas && n > this.carrilPintadas) {
889
+ s += this.destapar();
890
+ const ocupadas = Math.min(this.volcadas, Math.max(0, this.alto - this.carrilPintadas));
891
+ const subir = Math.max(0, ocupadas - Math.max(0, this.alto - n));
892
+ if (subir)
893
+ s += `\x1b[${this.alto};1H${'\r\n'.repeat(subir)}`;
894
+ }
895
+ if (this.carrilPintadas && n < this.carrilPintadas) {
896
+ s += this.destapar();
897
+ const topNuevo = this.filaDelCarril(n);
898
+ for (let fila = 1; fila < topNuevo; fila++)
899
+ s += `\x1b[${fila};1H\x1b[2K${this.lineaDeFila(fila, n)}`;
900
+ }
622
901
  }
623
902
  this.carrilPintadas = n;
624
903
  const top = this.filaDelCarril(n);
904
+ if (this.vistaSucia) {
905
+ for (let fila = 1; fila < top; fila++)
906
+ s += `\x1b[${fila};1H\x1b[2K${this.lineaDeFila(fila, n)}`;
907
+ this.vistaSucia = false;
908
+ this.encimaPintadas = 0;
909
+ }
625
910
  for (let i = k.encima.length; i < this.encimaPintadas; i++) {
626
911
  const fila = top - 1 - i;
627
912
  if (fila >= 1)
@@ -636,6 +921,12 @@ export class Superficie {
636
921
  k.carril.forEach((l, i) => {
637
922
  s += `\x1b[${top + i};1H\x1b[2K${l}`;
638
923
  });
924
+ this.zonas = [
925
+ ...k.zonasEncima.map((z) => ({ ...z, fila: top - k.encima.length + z.fila })),
926
+ ...k.zonas.map((z) => ({ ...z, fila: top + z.fila })),
927
+ ];
928
+ if (this.sobre && !this.zonas.some((z) => z.id === this.sobre))
929
+ this.sobre = null;
639
930
  if (k.cursor)
640
931
  s += `\x1b[${top + k.cursor.fila};${k.cursor.col + 1}H\x1b[?25h`;
641
932
  s += '\x1b[?2026l';
@@ -659,9 +950,13 @@ export class Superficie {
659
950
  this.consejosVisibles = false;
660
951
  if (!this.activa)
661
952
  return;
662
- this.lineas = this.bloques.flatMap((b) => b(this.ancho));
663
- if (this.lineas.length > MAX_LINEAS)
664
- this.lineas.splice(0, this.lineas.length - MAX_LINEAS);
953
+ if (this.subidas > 0 || this.volcadas < this.lineas.length) {
954
+ this.recomponer();
955
+ return;
956
+ }
957
+ this.componerTodo();
958
+ this.volcadas = this.lineas.length;
959
+ this.recortarMemoria();
665
960
  const n = componerCarril(this.datos(), this.ancho, this.alto).carril.length;
666
961
  const top = this.filaDelCarril(n);
667
962
  let s = '\x1b[?2026h\x1b[?25l';
@@ -672,41 +967,68 @@ export class Superficie {
672
967
  this.escribir(s);
673
968
  this.pintar();
674
969
  }
970
+ terminaEnBlanco() {
971
+ const ultima = this.lineas[this.lineas.length - 1];
972
+ return ultima === undefined || ultima.trim() === '';
973
+ }
675
974
  anadir(b) {
676
975
  this.ocultarConsejos();
677
- const destapado = this.activa ? this.destapar() : '';
678
- const n = this.activa ? this.carrilPintadas || componerCarril(this.datos(), this.ancho, this.alto).carril.length : 0;
679
- const desde = this.activa ? this.filaTrasLaConversacion(n) : 0;
680
976
  this.bloques.push(b);
681
977
  if (this.bloques.length > MAX_BLOQUES)
682
978
  this.bloques.splice(0, this.bloques.length - MAX_BLOQUES);
683
- const nuevas = b(this.ancho);
979
+ this.ultimoTrasBlanco = this.terminaEnBlanco();
980
+ const nuevas = sinBlancoDoble(b(this.ancho), this.ultimoTrasBlanco);
981
+ this.ultimoAlto = nuevas.length;
982
+ if (this.activa && (this.subidas > 0 || this.volcadas < this.lineas.length)) {
983
+ this.lineas.push(...nuevas);
984
+ this.recortarMemoria();
985
+ this.pintar();
986
+ return;
987
+ }
988
+ const destapado = this.activa ? this.destapar() : '';
684
989
  this.lineas.push(...nuevas);
685
- if (this.lineas.length > MAX_LINEAS)
686
- this.lineas.splice(0, this.lineas.length - MAX_LINEAS);
687
- if (!this.activa)
990
+ if (!this.activa) {
991
+ this.volcadas = this.lineas.length;
992
+ this.recortarMemoria();
993
+ return;
994
+ }
995
+ this.escribir(destapado);
996
+ this.volcar();
997
+ this.pintar();
998
+ }
999
+ volcar() {
1000
+ const nuevas = this.lineas.slice(this.volcadas);
1001
+ if (!nuevas.length)
688
1002
  return;
689
- let s = `\x1b[?2026h\x1b[?25l${destapado}`;
690
- s += `\x1b[${desde};1H\x1b[J`;
1003
+ const n = this.carrilPintadas || componerCarril(this.datos(), this.ancho, this.alto).carril.length;
1004
+ const desde = this.filaTrasLaConversacion(n);
1005
+ let s = `\x1b[?2026h\x1b[?25l\x1b[${desde};1H\x1b[J`;
691
1006
  s += nuevas.map((l) => `${l}\x1b[K\r\n`).join('');
692
1007
  s += '\r\n'.repeat(Math.max(0, n - 1));
693
1008
  s += '\x1b[?2026l';
1009
+ this.volcadas = this.lineas.length;
1010
+ this.recortarMemoria();
1011
+ this.encimaPintadas = 0;
694
1012
  this.escribir(s);
695
- this.pintar();
696
1013
  }
697
1014
  ultimoBloque() {
698
1015
  return this.bloques[this.bloques.length - 1];
699
1016
  }
700
1017
  reemplazarUltimo(b) {
701
1018
  const viejo = this.ultimoBloque();
702
- const k = viejo ? viejo(this.ancho).length : -1;
703
- const nuevas = b(this.ancho);
1019
+ const k = viejo ? this.ultimoAlto : -1;
1020
+ const nuevas = sinBlancoDoble(b(this.ancho), this.ultimoTrasBlanco);
704
1021
  if (!viejo || !this.activa || nuevas.length !== k || this.encimaPintadas > 0 || this.lineas.length < k) {
705
1022
  this.anadir(b);
706
1023
  return;
707
1024
  }
708
1025
  this.bloques[this.bloques.length - 1] = b;
709
1026
  this.lineas.splice(this.lineas.length - k, k, ...nuevas);
1027
+ if (this.subidas > 0 || this.volcadas < this.lineas.length) {
1028
+ this.vistaSucia = true;
1029
+ this.pintar();
1030
+ return;
1031
+ }
710
1032
  const n = this.carrilPintadas || componerCarril(this.datos(), this.ancho, this.alto).carril.length;
711
1033
  const primera = this.lineas.length - k - this.desplazadas(n) + 1;
712
1034
  let s = '\x1b[?2026h\x1b[?25l';
@@ -724,9 +1046,10 @@ export class Superficie {
724
1046
  return;
725
1047
  fijarCapacidades({ ancho: this.ancho });
726
1048
  this.anchoCompuesto = this.ancho;
727
- this.lineas = this.bloques.flatMap((b) => b(this.ancho));
728
- if (this.lineas.length > MAX_LINEAS)
729
- this.lineas.splice(0, this.lineas.length - MAX_LINEAS);
1049
+ this.componerTodo();
1050
+ this.volcadas = this.lineas.length;
1051
+ this.subidas = 0;
1052
+ this.recortarMemoria();
730
1053
  const n = componerCarril(this.datos(), this.ancho, this.alto).carril.length;
731
1054
  let s = alEmpezar ? `\x1b[?2026h\x1b[?25l${'\r\n'.repeat(this.alto)}\x1b[H` : '\x1b[?2026h\x1b[?25l\x1b[H\x1b[2J\x1b[3J\x1b[H';
732
1055
  s += this.lineas.map((l) => `${l}\x1b[K\r\n`).join('');
@@ -737,6 +1060,113 @@ export class Superficie {
737
1060
  this.escribir(s);
738
1061
  this.pintar();
739
1062
  }
1063
+ filasDeVista() {
1064
+ return Math.max(1, this.filaDelCarril(this.carrilPintadas || 3) - 1);
1065
+ }
1066
+ desplazar(n) {
1067
+ if (!this.activa)
1068
+ return;
1069
+ const max = this.desplazadas(this.carrilPintadas || componerCarril(this.datos(), this.ancho, this.alto).carril.length);
1070
+ const nuevo = Math.max(0, Math.min(max, this.subidas + n));
1071
+ if (nuevo === 0) {
1072
+ if (this.subidas > 0 || this.volcadas < this.lineas.length)
1073
+ this.irAlFondo();
1074
+ return;
1075
+ }
1076
+ if (nuevo === this.subidas)
1077
+ return;
1078
+ this.subidas = nuevo;
1079
+ this.vistaSucia = true;
1080
+ this.pintar();
1081
+ }
1082
+ irAlFondo() {
1083
+ if (!this.activa || (this.subidas === 0 && this.volcadas === this.lineas.length))
1084
+ return;
1085
+ this.subidas = 0;
1086
+ const n = this.carrilPintadas || componerCarril(this.datos(), this.ancho, this.alto).carril.length;
1087
+ const top = this.filaDelCarril(n);
1088
+ let s = '\x1b[?2026h\x1b[?25l';
1089
+ for (let fila = 1; fila < top; fila++)
1090
+ s += `\x1b[${fila};1H\x1b[2K${this.lineaDeFila(fila, n)}`;
1091
+ s += '\x1b[?2026l';
1092
+ this.encimaPintadas = 0;
1093
+ this.escribir(s);
1094
+ this.volcar();
1095
+ this.pintar();
1096
+ }
1097
+ get _vista() {
1098
+ return { subidas: this.subidas, pendientes: this.lineas.length - this.volcadas };
1099
+ }
1100
+ fijarRaton(si) {
1101
+ this.raton = si;
1102
+ this.sobre = null;
1103
+ this.pulsado = null;
1104
+ if (this.activa)
1105
+ this.escribir(si ? RATON_SI : RATON_NO);
1106
+ if (!si)
1107
+ this.irAlFondo();
1108
+ this.pintar();
1109
+ }
1110
+ zonaEn(x, y) {
1111
+ const col = x - 1;
1112
+ for (const z of this.zonas)
1113
+ if (z.fila === y && col >= z.desde && col < z.hasta)
1114
+ return z.id;
1115
+ return null;
1116
+ }
1117
+ alRaton(k) {
1118
+ if (!this.raton)
1119
+ return;
1120
+ if (k.rueda) {
1121
+ this.desplazar(-k.rueda * 3);
1122
+ return;
1123
+ }
1124
+ const zona = this.zonaEn(k.x, k.y);
1125
+ if (k.mover && k.boton === 3) {
1126
+ if (zona !== this.sobre) {
1127
+ this.sobre = zona;
1128
+ this.pintar();
1129
+ }
1130
+ return;
1131
+ }
1132
+ if (k.boton !== 0 && !(k.soltar && k.boton === 3))
1133
+ return;
1134
+ if (!k.soltar) {
1135
+ if (!k.mover)
1136
+ this.pulsado = zona;
1137
+ return;
1138
+ }
1139
+ const pulsado = this.pulsado;
1140
+ this.pulsado = null;
1141
+ if (pulsado && zona === pulsado)
1142
+ this.activar(pulsado);
1143
+ }
1144
+ activar(id) {
1145
+ if (id === 'fondo')
1146
+ return this.irAlFondo();
1147
+ if (id === 'bandeja') {
1148
+ if (this.resolverLectura && !this.trabajo && !this.pregunta)
1149
+ this.o.alBandeja?.();
1150
+ return;
1151
+ }
1152
+ if (id === 'modo') {
1153
+ if (!this.pregunta)
1154
+ this.o.alCambiarModo?.();
1155
+ return;
1156
+ }
1157
+ const opcion = /^opcion:(\d+)$/.exec(id);
1158
+ if (opcion && this.pregunta)
1159
+ return this.responder(Number(opcion[1]));
1160
+ const menu = /^menu:(\d+)$/.exec(id);
1161
+ if (menu && !this.pregunta) {
1162
+ this.menuElegida = Number(menu[1]);
1163
+ this.tecla({ t: 'intro' });
1164
+ }
1165
+ }
1166
+ modosSi() {
1167
+ return `\x1b[?2004h\x1b[?1004h${this.raton ? RATON_SI : ''}`;
1168
+ }
1169
+ static MODOS_NO = `${RATON_NO}\x1b[?2004l\x1b[?1004l\x1b[?25h`;
740
1170
  iniciar(bienvenida, consejos = []) {
741
1171
  if (this.activa)
742
1172
  return;
@@ -744,7 +1174,7 @@ export class Superficie {
744
1174
  if (bienvenida)
745
1175
  this.bloques.push(bienvenida);
746
1176
  if (consejos.length) {
747
- const ocupado = (bienvenida ? bienvenida(this.ancho).length : 0) + 3;
1177
+ const ocupado = (bienvenida ? bienvenida(this.ancho).length : 0) + componerCarril(this.datos(), this.ancho, this.alto).carril.length;
748
1178
  let n = consejos.length;
749
1179
  while (n > 0 && ocupado + bloqueDeConsejos(() => consejos.slice(0, n))(this.ancho).length > this.alto)
750
1180
  n--;
@@ -753,7 +1183,7 @@ export class Superficie {
753
1183
  if (n > 0)
754
1184
  this.bloques.push(bloqueDeConsejos(() => (this.consejosVisibles ? this.consejos : [])));
755
1185
  }
756
- this.escribir('\x1b[?2004h\x1b[?1004h');
1186
+ this.escribir(this.modosSi());
757
1187
  try {
758
1188
  this.o.entrada.setRawMode?.(true);
759
1189
  }
@@ -799,12 +1229,13 @@ export class Superficie {
799
1229
  terminar(despedida) {
800
1230
  if (!this.activa)
801
1231
  return;
1232
+ this.irAlFondo();
802
1233
  const n = this.carrilPintadas || componerCarril(this.datos(), this.ancho, this.alto).carril.length;
803
1234
  let s = this.destapar();
804
1235
  s += `\x1b[${this.filaTrasLaConversacion(n)};1H\x1b[J`;
805
1236
  if (despedida)
806
- s += `${m}${despedida}\r\n`;
807
- s += '\x1b[?2004l\x1b[?1004l\x1b[?25h';
1237
+ s += `${this.terminaEnBlanco() ? '' : '\r\n'}${m}${despedida}\r\n`;
1238
+ s += Superficie.MODOS_NO;
808
1239
  this.escribir(s);
809
1240
  this.activa = false;
810
1241
  if (this.relojAnim)
@@ -817,6 +1248,8 @@ export class Superficie {
817
1248
  this.o.entrada.removeListener('data', this.oyente);
818
1249
  if (this.alRedimensionar)
819
1250
  this.o.salida.removeListener('resize', this.alRedimensionar);
1251
+ if (this.alReanudar)
1252
+ process.removeListener('SIGCONT', this.alReanudar);
820
1253
  try {
821
1254
  this.o.entrada.setRawMode?.(false);
822
1255
  }
@@ -827,21 +1260,65 @@ export class Superficie {
827
1260
  restaurarDeUrgencia() {
828
1261
  if (!this.activa)
829
1262
  return;
830
- this.o.salida.write('\x1b[?2004l\x1b[?1004l\x1b[?25h\x1b[?2026l');
1263
+ this.o.salida.write(`${Superficie.MODOS_NO}\x1b[?2026l`);
831
1264
  try {
832
1265
  this.o.entrada.setRawMode?.(false);
833
1266
  }
834
1267
  catch {
835
1268
  }
836
1269
  }
1270
+ suspender() {
1271
+ if (process.platform === 'win32' && !this.o.suspenderProceso)
1272
+ return;
1273
+ this.irAlFondo();
1274
+ const n = this.carrilPintadas || componerCarril(this.datos(), this.ancho, this.alto).carril.length;
1275
+ let s = this.destapar();
1276
+ s += `\x1b[${this.filaTrasLaConversacion(n)};1H\x1b[J`;
1277
+ s += Superficie.MODOS_NO;
1278
+ this.escribir(s);
1279
+ try {
1280
+ this.o.entrada.setRawMode?.(false);
1281
+ }
1282
+ catch {
1283
+ }
1284
+ if (!this.alReanudar) {
1285
+ this.alReanudar = () => this.reanudar();
1286
+ if (!this.o.suspenderProceso)
1287
+ process.on('SIGCONT', this.alReanudar);
1288
+ }
1289
+ if (this.o.suspenderProceso)
1290
+ this.o.suspenderProceso();
1291
+ else
1292
+ process.kill(process.pid, 'SIGTSTP');
1293
+ }
1294
+ reanudar() {
1295
+ if (!this.activa)
1296
+ return;
1297
+ try {
1298
+ this.o.entrada.setRawMode?.(true);
1299
+ }
1300
+ catch {
1301
+ }
1302
+ this.o.entrada.resume();
1303
+ const n = componerCarril(this.datos(), this.ancho, this.alto).carril.length;
1304
+ let s = `\x1b[?2026h\x1b[?25l${this.modosSi()}${'\r\n'.repeat(this.alto)}`;
1305
+ for (let fila = 1; fila < this.filaDelCarril(n); fila++)
1306
+ s += `\x1b[${fila};1H\x1b[2K${this.lineaDeFila(fila, n)}`;
1307
+ s += '\x1b[?2026l';
1308
+ this.carrilPintadas = n;
1309
+ this.encimaPintadas = 0;
1310
+ this.escribir(s);
1311
+ this.pintar();
1312
+ }
837
1313
  leer(ejemplo = null) {
838
1314
  this.ejemplo = ejemplo;
839
- if (this.cola !== null) {
840
- const r = this.cola;
841
- this.cola = null;
1315
+ const enCola = this.cola.shift();
1316
+ if (enCola !== undefined) {
1317
+ const r = enCola;
842
1318
  this.ejemplo = null;
843
- this.mostrarAviso(null);
844
- this.anadir(bloqueDeLaPersona(this.vistaDeEnvio(r)));
1319
+ this.mostrarAviso(this.cola.length ? tt('tui.en_cola_n', { n: this.cola.length }) : null);
1320
+ if (r.trim() && r.trim() !== '!')
1321
+ this.anadir(this.bloqueDeEnvio(r));
845
1322
  return Promise.resolve(this.expandir(r));
846
1323
  }
847
1324
  return new Promise((resolver) => {
@@ -852,6 +1329,15 @@ export class Superficie {
852
1329
  this.pintar();
853
1330
  });
854
1331
  }
1332
+ escribirYMandar(texto) {
1333
+ if (!this.libre)
1334
+ return;
1335
+ this.ponerValor(texto);
1336
+ this.enviar();
1337
+ }
1338
+ get libre() {
1339
+ return Boolean(this.resolverLectura) && !this.trabajo && !this.pregunta;
1340
+ }
855
1341
  trabajar(alParar) {
856
1342
  if (this.trabajo)
857
1343
  this.trabajo.alParar = alParar;
@@ -885,31 +1371,55 @@ export class Superficie {
885
1371
  this.pintar();
886
1372
  }
887
1373
  preguntar(texto, tipo = 'normal') {
1374
+ const o = opcionesDePregunta(tipo);
1375
+ const peligro = tipo === 'bypass';
888
1376
  return new Promise((resolver) => {
889
- const mostrar = () => {
890
- const o = opcionesDePregunta(tipo);
891
- this.pregunta = { texto, ...o, ...(tipo === 'bypass' ? { peligro: true } : {}), resolver };
892
- this.o.alPreguntar?.(texto);
893
- this.pintar();
894
- };
895
- if (this.pregunta)
896
- this.esperando.push(mostrar);
897
- else
898
- mostrar();
1377
+ this.mostrarPregunta({
1378
+ texto,
1379
+ opciones: o.opciones,
1380
+ cortas: o.cortas,
1381
+ elegida: o.elegida,
1382
+ ...(peligro ? { peligro: true } : {}),
1383
+ indiceNo: o.valores.indexOf('no'),
1384
+ siConY: o.valores[o.elegida] !== 'no' || peligro,
1385
+ }, (i) => resolver(i === null ? 'no' : (o.valores[i] ?? 'no')));
1386
+ });
1387
+ }
1388
+ elegir(texto, opciones, o = {}) {
1389
+ return new Promise((resolver) => {
1390
+ this.mostrarPregunta({
1391
+ texto,
1392
+ ...(o.detalle ? { detalle: o.detalle } : {}),
1393
+ opciones: opciones.map((x) => x.etiqueta),
1394
+ cortas: opciones.map((x) => x.corta ?? x.etiqueta),
1395
+ notas: opciones.map((x) => ({ nota: x.nota, recomendada: x.recomendada, peligro: x.peligro })),
1396
+ elegida: Math.max(0, Math.min(opciones.length - 1, o.elegida ?? 0)),
1397
+ ...(o.vertical ? { vertical: true } : {}),
1398
+ indiceNo: null,
1399
+ siConY: false,
1400
+ }, resolver);
899
1401
  });
900
1402
  }
1403
+ mostrarPregunta(p, resolver) {
1404
+ const mostrar = () => {
1405
+ this.pregunta = { ...p, resolver, sordaHasta: Date.now() + this.guardaDePregunta };
1406
+ this.o.alPreguntar?.(p.texto);
1407
+ this.pintar();
1408
+ };
1409
+ if (this.pregunta)
1410
+ this.esperando.push(mostrar);
1411
+ else
1412
+ mostrar();
1413
+ }
901
1414
  responder(i) {
902
1415
  const p = this.pregunta;
903
1416
  if (!p)
904
1417
  return;
905
- const r = p.valores[i] ?? 'no';
906
1418
  this.pregunta = null;
907
- const etiqueta = p.opciones[i] ?? tt('tui.no');
908
- if (r !== 'si' && !p.peligro)
909
- this.anadir(bloqueDeLinea(`${c.gris('?')} ${p.texto} ${c.gris('→')} ${r === 'no' ? c.gris(etiqueta) : c.marca(etiqueta)}`));
910
- else
911
- this.pintar();
912
- p.resolver(r);
1419
+ this.sobre = null;
1420
+ this.pulsado = null;
1421
+ this.pintar();
1422
+ p.resolver(i);
913
1423
  this.esperando.shift()?.();
914
1424
  }
915
1425
  mostrarAviso(texto, ms = 0) {
@@ -933,14 +1443,18 @@ export class Superficie {
933
1443
  }
934
1444
  expandir(v) {
935
1445
  return v
936
- .replace(/\uE000(\d+)/g, (_x, i) => this.pegados[Number(i)] ?? '')
937
- .replace(/\uE003(\d+)/g, (_x, i) => `\uE001${this.imagenes[Number(i)] ?? ''}\uE002`);
1446
+ .replace(/(\d+)/g, (_x, i) => this.pegados[Number(i)] ?? '')
1447
+ .replace(/(\d+)/g, (_x, i) => `${this.imagenes[Number(i)] ?? ''}`);
1448
+ }
1449
+ bloqueDeEnvio(crudo) {
1450
+ const v = crudo.trim();
1451
+ return v.startsWith('!') ? bloqueDeBash(this.vistaDeEnvio(v.slice(1).trim())) : bloqueDeLaPersona(this.vistaDeEnvio(v));
938
1452
  }
939
1453
  vistaDeEnvio(v) {
940
- return v.replace(/[\uE000\uE003]\d+/g, (x) => sinColorSimple(this.vistaDeUnidad(x)));
1454
+ return v.replace(/[]\d+/g, (x) => sinColorSimple(this.vistaDeUnidad(x)));
941
1455
  }
942
1456
  ponerValor(v) {
943
- this.unidades = v.match(/[\uE000\uE003]\d+|[\s\S]/gu) ?? [];
1457
+ this.unidades = v.match(/[]\d+|[\s\S]/gu) ?? [];
944
1458
  this.cursor = this.unidades.length;
945
1459
  }
946
1460
  insertar(texto) {
@@ -959,17 +1473,18 @@ export class Superficie {
959
1473
  if (v && this.historial[this.historial.length - 1] !== crudo)
960
1474
  this.historial.push(crudo);
961
1475
  this.posHistorial = this.historial.length;
1476
+ this.irAlFondo();
962
1477
  if (this.trabajo || !this.resolverLectura) {
963
1478
  if (v) {
964
- this.cola = crudo;
965
- this.mostrarAviso(tt('tui.en_cola'));
1479
+ this.cola.push(crudo);
1480
+ this.mostrarAviso(this.cola.length > 1 ? tt('tui.en_cola_n', { n: this.cola.length }) : tt('tui.en_cola'));
966
1481
  }
967
1482
  return;
968
1483
  }
969
1484
  const r = this.resolverLectura;
970
1485
  this.resolverLectura = null;
971
- if (v)
972
- this.anadir(bloqueDeLaPersona(this.vistaDeEnvio(crudo.trim())));
1486
+ if (v && v !== '!')
1487
+ this.anadir(this.bloqueDeEnvio(crudo));
973
1488
  else
974
1489
  this.pintar();
975
1490
  r(this.expandir(crudo));
@@ -986,6 +1501,14 @@ export class Superficie {
986
1501
  this.foco = k.t === 'foco_si';
987
1502
  return;
988
1503
  }
1504
+ if (k.t === 'raton')
1505
+ return this.alRaton(k);
1506
+ if (k.t === 'pagina_arriba' || k.t === 'pagina_abajo')
1507
+ return this.desplazar((k.t === 'pagina_arriba' ? 1 : -1) * Math.max(1, this.filasDeVista() - 2));
1508
+ if (k.t === 'fondo')
1509
+ return this.irAlFondo();
1510
+ if (k.t === 'suspender')
1511
+ return this.suspender();
989
1512
  if (this.pregunta)
990
1513
  return this.teclaDePregunta(k);
991
1514
  if (k.t !== 'ctrlc' && this.ctrlC && !this.trabajo) {
@@ -1007,13 +1530,13 @@ export class Superficie {
1007
1530
  const imagen = rutaDeImagenPegada(v);
1008
1531
  if (imagen) {
1009
1532
  this.imagenes.push(imagen);
1010
- this.unidades.splice(this.cursor, 0, `\uE003${this.imagenes.length - 1}`, ' ');
1533
+ this.unidades.splice(this.cursor, 0, `${this.imagenes.length - 1}`, ' ');
1011
1534
  this.cursor += 2;
1012
1535
  break;
1013
1536
  }
1014
1537
  if (v.includes('\n') || v.length > PEGADO_LARGO) {
1015
1538
  this.pegados.push(v);
1016
- this.unidades.splice(this.cursor, 0, `\uE000${this.pegados.length - 1}`);
1539
+ this.unidades.splice(this.cursor, 0, `${this.pegados.length - 1}`);
1017
1540
  this.cursor++;
1018
1541
  }
1019
1542
  else
@@ -1042,6 +1565,8 @@ export class Superficie {
1042
1565
  this.menuCerradoPara = valor;
1043
1566
  else if (this.trabajo)
1044
1567
  this.trabajo.alParar();
1568
+ else if (this.subidas > 0 || this.volcadas < this.lineas.length)
1569
+ this.irAlFondo();
1045
1570
  break;
1046
1571
  case 'borrar':
1047
1572
  if (this.cursor > 0) {
@@ -1083,6 +1608,8 @@ export class Superficie {
1083
1608
  this.cursor = 0;
1084
1609
  break;
1085
1610
  case 'fin':
1611
+ if (this.cursor >= this.unidades.length && (this.subidas > 0 || this.volcadas < this.lineas.length))
1612
+ this.irAlFondo();
1086
1613
  this.cursor = this.unidades.length;
1087
1614
  break;
1088
1615
  case 'borrar_linea':
@@ -1157,17 +1684,38 @@ export class Superficie {
1157
1684
  break;
1158
1685
  }
1159
1686
  }
1687
+ get guardaDePregunta() {
1688
+ return this.o.guardaDePreguntaMs ?? (this.o.quieta ? 0 : 600);
1689
+ }
1160
1690
  teclaDePregunta(k) {
1161
1691
  const p = this.pregunta;
1162
1692
  const n = p.opciones.length;
1693
+ const ahora = Date.now();
1694
+ if (k.t === 'esc' || k.t === 'ctrlc')
1695
+ return this.responder(p.indiceNo);
1696
+ const r0 = k.t === 'texto' ? k.v.trim().toLowerCase() : '';
1697
+ const esRespuesta = k.t === 'texto' && (/^[1-9]$/.test(r0) || ((r0 === 'y' || r0 === 'n') && p.indiceNo !== null));
1698
+ if (k.t === 'texto' && !esRespuesta) {
1699
+ this.insertar(k.v);
1700
+ p.escribiendo = true;
1701
+ p.sordaHasta = Math.max(p.sordaHasta, ahora + this.guardaDePregunta);
1702
+ return;
1703
+ }
1704
+ if (ahora < p.sordaHasta) {
1705
+ if (k.t === 'texto' && p.escribiendo) {
1706
+ this.insertar(k.v);
1707
+ p.sordaHasta = ahora + this.guardaDePregunta;
1708
+ }
1709
+ return;
1710
+ }
1163
1711
  if (k.t === 'texto') {
1164
1712
  const r = k.v.trim().toLowerCase();
1165
1713
  const num = Number(r);
1166
1714
  if (Number.isInteger(num) && num >= 1 && num <= n)
1167
1715
  return this.responder(num - 1);
1168
- if (r === 'n')
1169
- return this.responder(p.valores.indexOf('no'));
1170
- if (r === 'y' && (p.valores[p.elegida] !== 'no' || p.peligro))
1716
+ if (r === 'n' && p.indiceNo !== null)
1717
+ return this.responder(p.indiceNo);
1718
+ if (r === 'y' && p.siConY)
1171
1719
  return this.responder(0);
1172
1720
  return;
1173
1721
  }
@@ -1177,8 +1725,6 @@ export class Superficie {
1177
1725
  p.elegida = (p.elegida + 1) % n;
1178
1726
  else if (k.t === 'intro')
1179
1727
  this.responder(p.elegida);
1180
- else if (k.t === 'esc' || k.t === 'ctrlc')
1181
- this.responder(p.valores.indexOf('no'));
1182
1728
  }
1183
1729
  get _lineas() {
1184
1730
  return this.lineas;
@@ -1192,6 +1738,9 @@ export class Superficie {
1192
1738
  get _trabajo() {
1193
1739
  return this.trabajo;
1194
1740
  }
1741
+ get _zonas() {
1742
+ return this.zonas;
1743
+ }
1195
1744
  }
1196
1745
  function sinColorSimple(t) {
1197
1746
  return t.replace(/\x1b\[[0-9;]*m/g, '');