@glassnote/client 2.4.11 → 2.4.13

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/linuxCursor.js ADDED
@@ -0,0 +1,187 @@
1
+ // Donde esta el mouse, preguntado directo al servidor X.
2
+ //
3
+ // Hace falta porque screen.getCursorScreenPoint() de Electron miente justo en el unico caso
4
+ // que le importa a un overlay: mientras la ventana ignora el mouse (input shape de 1x1 px en
5
+ // X11) Chromium no recibe un solo evento de puntero, y esa funcion devuelve para siempre la
6
+ // ultima posicion que alcanzo a ver. Medido:
7
+ //
8
+ // ventana ignorando -> sigue al mouse (cae al XQueryPointer del servidor)
9
+ // ventana clickeable -> sigue al mouse (le llegan eventos reales)
10
+ // vuelve a ignorar -> CONGELADA en el ultimo punto que vio, para siempre
11
+ //
12
+ // O sea: funciona hasta la primera interaccion y despues miente. Ese es exactamente el
13
+ // "andaba y dejo de andar" del passthrough en Linux.
14
+ //
15
+ // Asi que se le pregunta al servidor: XQueryPointer, opcode 38. Es protocolo binario a mano,
16
+ // pero es protocolo congelado hace decadas y no arrastra ninguna dependencia nativa —que en
17
+ // este cliente, que se instala con npx y sin compilador, no es un lujo sino un requisito—.
18
+ // Si algo falla (Wayland, display por TCP, sin cookie) se devuelve null y quien llama se las
19
+ // arregla con lo que tenga.
20
+ 'use strict';
21
+
22
+ const net = require('net');
23
+ const fs = require('fs');
24
+ const os = require('os');
25
+ const path = require('path');
26
+
27
+ const OPCODE_QUERY_POINTER = 38;
28
+
29
+ let socket = null;
30
+ let ventanaRaiz = null;
31
+ let ultimaPosicion = null;
32
+ let consultaEnVuelo = false;
33
+ let inutilizable = false;
34
+
35
+ // El .Xauthority es una lista de bloques con largo de 2 bytes big-endian: familia, direccion,
36
+ // numero de display, nombre del metodo y la cookie. Solo interesa la MIT-MAGIC-COOKIE-1 de
37
+ // este display (o la que no fija display, que vale para todos).
38
+ function leerCookie(numeroDisplay) {
39
+ const archivo = process.env.XAUTHORITY || path.join(os.homedir(), '.Xauthority');
40
+ let buf;
41
+ try {
42
+ buf = fs.readFileSync(archivo);
43
+ } catch (error) {
44
+ return null;
45
+ }
46
+
47
+ let i = 0;
48
+ const bloque = () => {
49
+ if (i + 2 > buf.length) return null;
50
+ const largo = buf.readUInt16BE(i);
51
+ i += 2;
52
+ if (i + largo > buf.length) return null;
53
+ const datos = buf.subarray(i, i + largo);
54
+ i += largo;
55
+ return datos;
56
+ };
57
+
58
+ while (i + 2 <= buf.length) {
59
+ i += 2; // familia
60
+ const direccion = bloque();
61
+ const numero = bloque();
62
+ const nombre = bloque();
63
+ const cookie = bloque();
64
+ if (!direccion || !numero || !nombre || !cookie) break;
65
+ if (nombre.toString() !== 'MIT-MAGIC-COOKIE-1') continue;
66
+ const suDisplay = numero.toString();
67
+ if (suDisplay === '' || suDisplay === String(numeroDisplay)) return { nombre, cookie };
68
+ }
69
+ return null;
70
+ }
71
+
72
+ const relleno = (largo) => (4 - (largo % 4)) % 4;
73
+
74
+ function numeroDeDisplay() {
75
+ const display = process.env.DISPLAY || '';
76
+ // Solo sockets unix locales: ":0", ":0.0". Un display por TCP ("host:0") no se atiende.
77
+ const coincide = /^:(\d+)/.exec(display);
78
+ return coincide ? parseInt(coincide[1], 10) : null;
79
+ }
80
+
81
+ function conectar() {
82
+ const numero = numeroDeDisplay();
83
+ if (numero === null) {
84
+ inutilizable = true;
85
+ return;
86
+ }
87
+
88
+ const auth = leerCookie(numero) || { nombre: Buffer.alloc(0), cookie: Buffer.alloc(0) };
89
+ let cliente;
90
+ try {
91
+ cliente = net.connect(`/tmp/.X11-unix/X${numero}`);
92
+ } catch (error) {
93
+ inutilizable = true;
94
+ return;
95
+ }
96
+
97
+ cliente.on('error', () => {
98
+ inutilizable = true;
99
+ socket = null;
100
+ ventanaRaiz = null;
101
+ });
102
+ cliente.on('close', () => {
103
+ socket = null;
104
+ ventanaRaiz = null;
105
+ consultaEnVuelo = false;
106
+ });
107
+
108
+ cliente.once('connect', () => {
109
+ const largoNombre = auth.nombre.length;
110
+ const largoCookie = auth.cookie.length;
111
+ const inicio = Buffer.alloc(
112
+ 12 + largoNombre + relleno(largoNombre) + largoCookie + relleno(largoCookie)
113
+ );
114
+ inicio.write('l', 0, 'ascii'); // little endian
115
+ inicio.writeUInt16LE(11, 2); // protocolo 11.0
116
+ inicio.writeUInt16LE(0, 4);
117
+ inicio.writeUInt16LE(largoNombre, 6);
118
+ inicio.writeUInt16LE(largoCookie, 8);
119
+ auth.nombre.copy(inicio, 12);
120
+ auth.cookie.copy(inicio, 12 + largoNombre + relleno(largoNombre));
121
+ cliente.write(inicio);
122
+
123
+ cliente.once('data', (respuesta) => {
124
+ if (respuesta[0] !== 1) {
125
+ // 0 = rechazado (cookie mala), 2 = pide mas autenticacion.
126
+ inutilizable = true;
127
+ cliente.destroy();
128
+ return;
129
+ }
130
+ // La ventana raiz esta despues del vendor y de los formatos de pixel, todos de
131
+ // largo variable: hay que saltearlos para llegar a la primera pantalla.
132
+ const largoVendor = respuesta.readUInt16LE(24);
133
+ const cantidadFormatos = respuesta[29];
134
+ const inicioPantallas = 40 + largoVendor + relleno(largoVendor) + 8 * cantidadFormatos;
135
+ if (inicioPantallas + 4 > respuesta.length) {
136
+ inutilizable = true;
137
+ cliente.destroy();
138
+ return;
139
+ }
140
+ ventanaRaiz = respuesta.readUInt32LE(inicioPantallas);
141
+ socket = cliente;
142
+
143
+ cliente.on('data', (paquete) => {
144
+ // Respuesta de QueryPointer: 32 bytes, root_x y root_y en 20 y 22.
145
+ if (paquete.length >= 32 && paquete[0] === 1) {
146
+ ultimaPosicion = { x: paquete.readInt16LE(20), y: paquete.readInt16LE(22) };
147
+ }
148
+ consultaEnVuelo = false;
149
+ });
150
+ });
151
+ });
152
+ }
153
+
154
+ // Pide la posicion al servidor y devuelve la ULTIMA que contesto: la respuesta llega por el
155
+ // socket, no en el acto. Consultando a 30 Hz eso son 30 ms de atraso, que para decidir si el
156
+ // cursor esta sobre un boton no se nota. Nunca hay mas de una consulta en vuelo: si el
157
+ // servidor se atrasa, no se le encima una cola de pedidos que ya no le sirven a nadie.
158
+ function posicion() {
159
+ if (inutilizable) return null;
160
+ if (!socket) {
161
+ conectar();
162
+ return ultimaPosicion;
163
+ }
164
+ if (!consultaEnVuelo) {
165
+ const pedido = Buffer.alloc(8);
166
+ pedido[0] = OPCODE_QUERY_POINTER;
167
+ pedido.writeUInt16LE(2, 2); // largo en palabras de 4 bytes
168
+ pedido.writeUInt32LE(ventanaRaiz, 4);
169
+ consultaEnVuelo = true;
170
+ try {
171
+ socket.write(pedido);
172
+ } catch (error) {
173
+ consultaEnVuelo = false;
174
+ }
175
+ }
176
+ return ultimaPosicion;
177
+ }
178
+
179
+ function detener() {
180
+ if (socket) socket.destroy();
181
+ socket = null;
182
+ ventanaRaiz = null;
183
+ ultimaPosicion = null;
184
+ consultaEnVuelo = false;
185
+ }
186
+
187
+ module.exports = { posicion, detener };
package/main.js CHANGED
@@ -13,14 +13,28 @@ const userData = require('./userData');
13
13
  const { ipcMain } = require('electron');
14
14
  const path = require('path');
15
15
  const fs = require('fs');
16
+ const { execFile } = require('child_process');
16
17
  const AutoStart = require('./autoStart');
17
18
  const AutoUpdate = require('./autoupdate');
18
19
  const NpmUpdate = require('./npmUpdate');
19
20
  const VersionReport = require('./versionReport');
20
21
  const npmMode = require('./npmMode');
21
22
  const AutoUpdateRenderer = require('./autoupdate_renderer');
22
- const { baselineRendererDir, rendererVersionFrom: getRendererVersionFromPath } = require('./rendererPaths');
23
+ const { resolverRenderer } = require('./rendererPaths');
24
+ const cursorX11 = require('./linuxCursor');
23
25
  const { createTailUtility } = require('./logUtilities');
26
+ const { crashReporter } = require('electron');
27
+
28
+ // Sin esto un crash nativo —del proceso principal o de un hijo— no deja absolutamente
29
+ // nada: ni log, ni volcado, y desde afuera se ve igual que un cierre voluntario. No se
30
+ // sube a ningun lado. Los minidumps NO caen junto a los logs: van a la carpeta que
31
+ // Electron llama crashDumps, que se arma con el nombre del paquete y queda en otro arbol
32
+ // (<AppData>/@glassnote/client/Crashpad). Por eso el arranque la escribe en el log.
33
+ try {
34
+ crashReporter.start({ uploadToServer: false });
35
+ } catch (error) {
36
+ console.error('No se pudo iniciar el crashReporter:', error.message);
37
+ }
24
38
 
25
39
  const logRenderer = packageJson.logRenderer !== undefined ? packageJson.logRenderer : true;
26
40
 
@@ -33,7 +47,12 @@ let mainWindow;
33
47
  let localServer = null;
34
48
  let tray = null;
35
49
  let lockCheckerInterval = null;
36
- let lockCheckerTimeout = null;
50
+ let sondeoCursor = null;
51
+ let ultimoPuntoEnviado = { x: null, y: null };
52
+ let ultimoEnvioMs = 0;
53
+ let cursorFueraDeLaVentana = false;
54
+ let passthroughPedidoPorRenderer = true;
55
+ let ignorandoMouse = true;
37
56
  let isCleaningUp = false;
38
57
 
39
58
 
@@ -53,15 +72,22 @@ function cleanupOldLogs() {
53
72
  const now = Date.now();
54
73
  const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
55
74
 
75
+ const borrados = [];
56
76
  files.forEach((file) => {
57
77
  if (file.endsWith('.log')) {
58
78
  const filePath = path.join(logsDir, file);
59
79
  const stats = fs.statSync(filePath);
60
80
  if (stats.mtimeMs < thirtyDaysAgo) {
61
81
  fs.unlinkSync(filePath);
82
+ borrados.push(file);
62
83
  }
63
84
  }
64
85
  });
86
+ if (borrados.length) {
87
+ console.warn(
88
+ `[logs] borrados ${borrados.length} de mas de 30 dias: ${borrados.join(', ')}`
89
+ );
90
+ }
65
91
  } catch (error) {
66
92
  console.error('Error cleaning up old logs:', error);
67
93
  }
@@ -92,7 +118,67 @@ console.log = function (...args) {
92
118
  // Call original console.log
93
119
  originalConsoleLog.apply(console, args);
94
120
  };
121
+ // console.warn no estaba interceptado y se perdian los 10 avisos del proceso principal,
122
+ // justo los que dicen que algo raro paso pero todavia no es un error: entre ellos el
123
+ // marcador de arranque de aca abajo y el "el archivo actual no es legible" de userData,
124
+ // que es exactamente lo que hace falta para entender un cierre silencioso.
125
+ const originalConsoleWarn = console.warn;
126
+ console.warn = function (...args) {
127
+ const timestamp = new Date().toISOString();
128
+ const message = args
129
+ .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
130
+ .join(' ');
131
+
132
+ const logMessage = `[${timestamp}] WARN: ${message}\n`;
133
+
134
+ try {
135
+ fs.appendFileSync(logFilePath, logMessage, 'utf8');
136
+ } catch (error) {
137
+ originalConsoleWarn.apply(console, ['Error writing to log file:', error]);
138
+ }
139
+
140
+ originalConsoleWarn.apply(console, args);
141
+ };
142
+
143
+ // Marca de cierre limpio. Es lo unico que distingue "la app se cerro sola" de "a la app
144
+ // la mataron", porque un crash o un apagado no llegan a borrarla. Se escribe en su propio
145
+ // archivo y no en .glassnote, que es el del lock: tocar ese al salir seria pedirle a otra
146
+ // instancia que se cierre.
147
+ const marcaCierrePath = path.join(userData.getUserDataDir(), 'cierre-limpio');
148
+
149
+ function huboCierreLimpio() {
150
+ try {
151
+ if (!fs.existsSync(marcaCierrePath)) return null;
152
+ const contenido = fs.readFileSync(marcaCierrePath, 'utf8').trim();
153
+ fs.unlinkSync(marcaCierrePath);
154
+ return contenido || 'sin motivo';
155
+ } catch (error) {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ function marcarCierreLimpio(motivo) {
161
+ try {
162
+ fs.mkdirSync(userData.getUserDataDir(), { recursive: true });
163
+ fs.writeFileSync(marcaCierrePath, String(motivo), 'utf8');
164
+ } catch (error) {
165
+ console.error('No se pudo dejar la marca de cierre limpio:', error.message);
166
+ }
167
+ }
168
+
169
+ const cierreAnterior = huboCierreLimpio();
170
+
95
171
  console.warn('====== APPLICATION START ======');
172
+ console.warn(
173
+ `arranque: v${packageJson.version} ${process.platform} ${process.arch} ` +
174
+ `electron ${process.versions.electron} node ${process.versions.node} pid ${process.pid}`
175
+ );
176
+ console.warn(`volcados de crash en: ${app.getPath('crashDumps')}`);
177
+ console.warn(
178
+ cierreAnterior
179
+ ? `el cierre anterior fue limpio (${cierreAnterior})`
180
+ : `ATENCION: el arranque anterior NO cerro limpio; se murio o lo mataron`
181
+ );
96
182
  let initialLock = Date.now().toString();
97
183
 
98
184
  // Override console.error as well
@@ -117,7 +203,13 @@ console.error = function (...args) {
117
203
  };
118
204
 
119
205
  // Clean up old logs on startup
206
+ // Corria una sola vez, al arrancar, y este cliente esta pensado para quedarse prendido
207
+ // semanas: mientras siguiera vivo los logs de mas de 30 dias no se borraban nunca. Con el
208
+ // log mas hablador eso se nota antes, asi que ademas se repasa una vez por dia. unref()
209
+ // para que este reloj no sea motivo de que el proceso siga en pie.
210
+ const LIMPIEZA_LOGS_MS = 24 * 60 * 60 * 1000;
120
211
  cleanupOldLogs();
212
+ setInterval(cleanupOldLogs, LIMPIEZA_LOGS_MS).unref();
121
213
 
122
214
  // Create tail utility in logs directory for debugging
123
215
  createTailUtility(logsDir);
@@ -173,6 +265,8 @@ app.whenReady().then(() => {
173
265
  const versionReport = new VersionReport();
174
266
  versionReport.start();
175
267
 
268
+ avisarSiFaltaCompositor();
269
+
176
270
  // Create system tray with platform-appropriate icon
177
271
  let iconPath;
178
272
  if (process.platform === 'win32') {
@@ -183,32 +277,34 @@ app.whenReady().then(() => {
183
277
  } else {
184
278
  iconPath = path.join(__dirname, 'images/icon.png');
185
279
  }
186
- tray = new Tray(iconPath);
187
- const contextMenu = Menu.buildFromTemplate([
188
- {
189
- label: `glassnote v${packageJson.version}`,
190
- enabled: false,
191
- },
192
- { type: 'separator' },
193
- {
194
- label: 'Config Menu',
195
- click: () => {
196
- // Enviar mensaje al renderer para mostrar el ConfigMenu con vista Review
197
- if (mainWindow && !mainWindow.isDestroyed()) {
198
- mainWindow.webContents.send('show-config-menu', 'review');
199
- }
280
+ // En Linux el icono de bandeja viaja por D-Bus (StatusNotifierItem) y Electron solo manda
281
+ // el mapa de bits. Los paneles cuyo watcher es ayatana-indicator-application (XFCE con el
282
+ // plugin Indicator, Unity, MATE) exigen ademas IconName y descartan el item completo si
283
+ // falta: el icono no aparece en ningun lado y no hay error. Chromium si escribe el icono a
284
+ // un archivo y manda IconName, pero solo cuando cree estar en GNOME o Pantheon
285
+ // (ShouldWriteIconToFile, status_icon_linux_dbus.cc). Eso se lee una unica vez, dentro del
286
+ // constructor de Tray, asi que se miente para esa linea y se restaura enseguida: al resto
287
+ // de Chromium (dialogos, decoraciones) le sigue constando el escritorio real.
288
+ const realDesktop = process.env.XDG_CURRENT_DESKTOP;
289
+ if (process.platform === 'linux') {
290
+ process.env.XDG_CURRENT_DESKTOP = 'GNOME';
291
+ }
292
+ try {
293
+ tray = new Tray(iconPath);
294
+ } finally {
295
+ if (process.platform === 'linux') {
296
+ if (realDesktop === undefined) {
297
+ delete process.env.XDG_CURRENT_DESKTOP;
298
+ } else {
299
+ process.env.XDG_CURRENT_DESKTOP = realDesktop;
200
300
  }
201
301
  }
202
- ]);
203
- tray.setToolTip(`GlassNote v${packageJson.version}`);
204
- tray.setContextMenu(contextMenu);
302
+ }
303
+ actualizarMenuTray();
205
304
 
206
305
  // Agregar funcionalidad de click izquierdo para mostrar el Config Menu
207
306
  tray.on('click', () => {
208
- // Enviar mensaje al renderer para mostrar el ConfigMenu con vista Review
209
- if (mainWindow && !mainWindow.isDestroyed()) {
210
- mainWindow.webContents.send('show-config-menu', 'review');
211
- }
307
+ abrirConfigMenu('review');
212
308
  });
213
309
 
214
310
  // Platform-specific window configuration
@@ -246,6 +342,39 @@ app.whenReady().then(() => {
246
342
  // es el borde negro que se ve alrededor de los mensajes. Una capa de overlay no
247
343
  // necesita sombra.
248
344
  windowOptions.hasShadow = false;
345
+ } else if (process.platform === 'linux') {
346
+ // En Linux fullscreen es justo lo que NO hay que pedir. Un gestor de ventanas
347
+ // tipico —xfwm4, y no es el unico— trata "pantalla completa" como una app que se
348
+ // adueña del monitor: mientras tiene el foco la deja arriba de todo, y en cuanto se
349
+ // lo lleva otra ventana la BAJA, para que se pueda salir de ella. Medido en XFCE:
350
+ //
351
+ // fullscreen: true -> _NET_WM_STATE_FULLSCREEN, sin ABOVE; al enfocar otra
352
+ // ventana el overlay cae de la posicion 14/14 a la 9/14
353
+ // del tamaño de la pantalla -> _NET_WM_STATE_ABOVE, se queda en 14/14
354
+ //
355
+ // O sea que el alwaysOnTop solo se respeta si la ventana NO es fullscreen: pedir las
356
+ // dos cosas a la vez es pedir que se ignore la que importa.
357
+ //
358
+ // Va al AREA UTIL y no a la pantalla entera porque una ventana normal no puede
359
+ // meterse debajo de los paneles: si se le pide la pantalla completa, el gestor la
360
+ // empuja igual —queda en (0,25) y con 25 px afuera— y encima el tamaño le baja un
361
+ // pixel solo. Pidiendo el area util la geometria sale exacta y estable, que es lo que
362
+ // necesita el renderer para colocar los glasses.
363
+ const areaUtil = screen.getPrimaryDisplay().workArea;
364
+ windowOptions.x = areaUtil.x;
365
+ windowOptions.y = areaUtil.y;
366
+ windowOptions.width = areaUtil.width;
367
+ windowOptions.height = areaUtil.height;
368
+ windowOptions.fullscreen = false;
369
+ // `skipTaskbar` no existe en Linux —Electron solo lo implementa para Windows y macOS—
370
+ // asi que el overlay salia como una ventana normal y el panel le ponia su boton, que
371
+ // para una capa transparente no tiene ningun sentido. Se arregla por el tipo de
372
+ // ventana: TOOLBAR sale con SKIP_TASKBAR y SKIP_PAGER puestos por el gestor.
373
+ //
374
+ // De los tipos que Linux acepta es el unico que sirve: `splash` tambien se esconde
375
+ // pero NO se puede enfocar, y sin foco el ConfigMenu se queda sin teclado;
376
+ // `notification` se enfoca pero igual aparece en la barra.
377
+ windowOptions.type = 'toolbar';
249
378
  } else {
250
379
  // Windows and other platforms
251
380
  windowOptions.width = 400;
@@ -358,22 +487,90 @@ app.whenReady().then(() => {
358
487
  // Cargar el HTML del renderer (priorizar userData, fallback a app)
359
488
  loadRendererHtml(mainWindow, uuid);
360
489
 
361
- // Mouse events control
490
+ // Mouse events control (en Linux el forward lo emula iniciarSondeoCursor)
362
491
  mainWindow.setIgnoreMouseEvents(true, { forward: true });
492
+ iniciarSondeoCursor();
363
493
  ipcMain.on('set-ignore-events-true', () => {
364
- mainWindow.setIgnoreMouseEvents(true, { forward: true });
494
+ pedirPassthrough(true);
365
495
  });
366
496
  ipcMain.on('set-ignore-events-false', () => {
367
- mainWindow.setIgnoreMouseEvents(false, { forward: true });
497
+ pedirPassthrough(false);
368
498
  });
369
499
 
370
500
  // Window visibility control
501
+ //
502
+ // showInactive y no show: un glass es una notificacion, no tiene por que sacarle el
503
+ // teclado a lo que el usuario este haciendo. show() enfoca la ventana, y como el
504
+ // overlay ocupa toda la pantalla eso equivale a interrumpir. El unico que necesita
505
+ // foco es el ConfigMenu, y ese lo pide aparte.
371
506
  ipcMain.on('show-window', () => {
372
507
  if (mainWindow && !mainWindow.isDestroyed()) {
373
- mainWindow.show();
508
+ mainWindow.showInactive();
374
509
  }
375
510
  });
376
511
 
512
+ // En X11 la ventana pierde la input shape al volver a mostrarse, y con ella el
513
+ // passthrough: el overlay se queda comiendo los clics de TODA la pantalla aunque las dos
514
+ // mitades del programa crean que esta dejando pasar el mouse. Medido con la ventana X que
515
+ // hay bajo el puntero, que es quien manda de verdad:
516
+ //
517
+ // sin nada en pantalla -> la ventana de abajo (pasa el clic)
518
+ // despues de show() -> el overlay (se lo come)
519
+ //
520
+ // Por eso el glass del splash se portaba bien —la ventana ya estaba mapeada desde el
521
+ // arranque— y cualquier mensaje posterior bloqueaba: cada uno llega con su show(). Se
522
+ // reaplica en el evento, no en el handler del IPC, para cubrir tambien los show() que
523
+ // salen de otro lado.
524
+ // Y la GEOMETRIA se pierde igual que la input shape. El gestor recoloca la ventana por
525
+ // su cuenta DESPUES de mapearla y la deja corrida 25 px hacia el monitor de al lado, asi
526
+ // que el primer glass salia bien y los siguientes no. Medido con xwininfo, que es quien
527
+ // dice la verdad: getBounds() de Electron devuelve lo que se pidio, no lo que quedo.
528
+ //
529
+ // pedido x=0 -> electron dice x=0, X11 dice x=25
530
+ //
531
+ // Pedirla una sola vez al mostrar es una carrera que se pierde la mitad de las veces
532
+ // (medido: 25, 0, 25 en tres glasses seguidos). Por eso se reacciona tambien al evento
533
+ // `move`, que es el gestor avisando que la movio, y se la vuelve a poner. El contador
534
+ // corta el ida y vuelta por si alguna vez el gestor se planta.
535
+ //
536
+ // Se le pide de la barra de arriba hasta el borde de abajo: asi la ventana entra justa
537
+ // en la pantalla, no se sale por la derecha, tapa el panel inferior y le deja al arte
538
+ // una proporcion mucho mas cerca de 16:9 que el area util.
539
+ let correccionesDeGeometria = 0;
540
+
541
+ function fijarGeometria() {
542
+ if (!mainWindow || mainWindow.isDestroyed()) return;
543
+ const pantalla = screen.getPrimaryDisplay();
544
+ mainWindow.setBounds({
545
+ x: 0,
546
+ y: pantalla.workArea.y,
547
+ width: pantalla.bounds.width,
548
+ height: pantalla.bounds.height - pantalla.workArea.y
549
+ });
550
+ }
551
+
552
+ mainWindow.on('move', () => {
553
+ if (process.platform !== 'linux') return;
554
+ if (correccionesDeGeometria >= 8) return;
555
+ correccionesDeGeometria++;
556
+ fijarGeometria();
557
+ });
558
+
559
+ mainWindow.on('show', () => {
560
+ if (process.platform === 'linux') {
561
+ correccionesDeGeometria = 0;
562
+ fijarGeometria();
563
+ }
564
+ reafirmarAlwaysOnTop();
565
+ aplicarPassthrough({ forzar: true });
566
+ });
567
+
568
+ // El canal ya estaba habilitado en el preload para renderer -> main, pero del lado de
569
+ // main no lo escuchaba nadie: el menu solo se podia abrir desde la bandeja.
570
+ ipcMain.on('show-config-menu', (event, vista) => {
571
+ abrirConfigMenu(vista);
572
+ });
573
+
377
574
  ipcMain.on('hide-window', () => {
378
575
  if (mainWindow && !mainWindow.isDestroyed()) {
379
576
  mainWindow.hide();
@@ -404,6 +601,9 @@ app.whenReady().then(() => {
404
601
  const registrationWindow = new BrowserWindow({
405
602
  width: 1200,
406
603
  height: 800,
604
+ // Sin esto Electron no pone _NET_WM_ICON y la unica ventana que sale en la barra de
605
+ // tareas de Linux aparece con el icono generico del gestor de ventanas.
606
+ icon: path.join(__dirname, 'images/icon.png'),
407
607
  minWidth: 800,
408
608
  minHeight: 600,
409
609
  show: false,
@@ -434,7 +634,7 @@ app.whenReady().then(() => {
434
634
  });
435
635
 
436
636
  mainWindow.on('ready-to-show', () => {
437
- mainWindow.show();
637
+ mainWindow.showInactive();
438
638
 
439
639
  // Use LocalServer singleton
440
640
  if (!localServer) {
@@ -449,12 +649,30 @@ app.whenReady().then(() => {
449
649
  lockChecker();
450
650
  });
451
651
 
652
+ // Un crash del renderer o de un hijo (GPU, utility) no pasa por ningun handler de los de
653
+ // arriba: no es una excepcion de JavaScript ni una senal, asi que el log se quedaba mudo.
654
+ // `reason` es lo que hay que leer: 'crashed', 'oom', 'killed', 'abnormal-exit',
655
+ // 'launch-failed', 'integrity-failure' o 'clean-exit'.
656
+ app.on('render-process-gone', (event, webContents, details) => {
657
+ console.error(
658
+ `====== MURIO EL RENDERER: ${details.reason} (exitCode ${details.exitCode}) ======`
659
+ );
660
+ });
661
+
662
+ app.on('child-process-gone', (event, details) => {
663
+ console.error(
664
+ `====== MURIO UN PROCESO HIJO: ${details.type}` +
665
+ `${details.name ? ' (' + details.name + ')' : ''} ` +
666
+ `${details.reason} (exitCode ${details.exitCode}) ======`
667
+ );
668
+ });
669
+
452
670
  app.on('window-all-closed', () => {
453
671
  // Don't quit when all windows are closed if we have a tray
454
672
  if (tray && process.platform !== 'darwin') {
455
673
  // Keep the app running with tray
456
674
  } else {
457
- cleanupAndQuit();
675
+ cleanupAndQuit('se cerraron todas las ventanas');
458
676
  }
459
677
  });
460
678
 
@@ -498,23 +716,199 @@ function lockChecker() {
498
716
  lockCheckerInterval = setInterval(() => {
499
717
  // Leer el lock directamente del archivo, no del cache
500
718
  const currentData = userData.getLockFromFile();
501
- // Si currentData es undefined, significa que el archivo .glassnote fue borrado o corrompido
502
- // También verificar si el valor del lock cambió
503
- if (currentData === undefined || currentData.toString() != initialLock.toString()) {
504
- cleanupAndQuit();
719
+
720
+ // Esta es la via por la que la app se cerraba sin dejar rastro, y las dos causas no
721
+ // son la misma cosa: que el archivo no este es un problema del disco o de algo que
722
+ // lo borro, y que el lock haya cambiado significa que arranco otra instancia y esta
723
+ // le cede el lugar. Se distinguen en el log porque llevan a sitios muy distintos.
724
+ if (currentData === undefined) {
725
+ cleanupAndQuit('el lock no se pudo leer: .glassnote no esta o no tiene lock');
726
+ return;
727
+ }
728
+ if (currentData.toString() != initialLock.toString()) {
729
+ cleanupAndQuit(
730
+ `el lock cambio (mio ${initialLock}, en el archivo ${currentData}): arranco otra instancia`
731
+ );
505
732
  }
506
733
  }, 10000); // Check every 10 seconds
507
734
  }
508
735
 
509
- function cleanupAndQuit() {
736
+ // El overlay deja pasar el mouse y solo se vuelve clickeable cuando el cursor esta encima
737
+ // de algo interactivo. Quien decide eso es el renderer: escucha `mousemove` en el document,
738
+ // compara la posicion contra los rectangulos de los elementos que registro, y pide apagar o
739
+ // encender el passthrough. Para decidir, entonces, necesita ver el mouse.
740
+ //
741
+ // En Windows y macOS se lo da `forward: true`. En Linux esa opcion NO EXISTE: Electron le
742
+ // pone al window una input shape de 1x1 px en X11 y el flag ni se mira (SetIgnoreMouseEvents
743
+ // en native_window_views.cc solo lo usa bajo IS_WIN). Sin un solo evento de mouse la
744
+ // posicion que guarda el renderer se queda en null para siempre, ningun elemento resulta
745
+ // nunca "bajo el cursor", y por eso en Linux no responde un boton.
746
+ //
747
+ // Se emula: se lee la posicion global del cursor y se le inyecta al renderer un mouseMove
748
+ // sintetico, que es exactamente lo que forwardea Windows. El hit-test sigue donde estan las
749
+ // zonas —en el renderer—; desde aca solo se le cuenta donde esta el mouse.
750
+ const SONDEO_CURSOR_MS = 30;
751
+ // Cada cuanto se repite la posicion aunque el cursor no se haya movido.
752
+ const REEMISION_QUIETO_MS = 400;
753
+
754
+ // El passthrough efectivo sale de dos cosas: lo que pidio el renderer y si el cursor esta
755
+ // siquiera sobre la ventana.
756
+ //
757
+ // Lo segundo no es un detalle: cuando el overlay esta clickeable y el cursor se va —a otro
758
+ // monitor, por ejemplo— el renderer no recibe un mousemove mas, se queda con la ultima
759
+ // posicion (encima del boton) y nunca devuelve el passthrough. El overlay se queda comiendo
760
+ // TODOS los clics de esa pantalla.
761
+ //
762
+ // No es solo de X11. Medido en Windows con dos monitores: al salir la ventana NO llega
763
+ // ningun mouseleave —se probo, y el evento recien aparece cuando el cursor vuelve—, asi que
764
+ // el overlay queda clickeable todo el tiempo que estes en la otra pantalla. Ahi se cura
765
+ // solo al volver, porque con la ventana clickeable los movimientos reales si le llegan;
766
+ // en Linux quedaba trabado hasta reiniciar. Con un solo monitor no se dispara: el overlay
767
+ // ocupa toda la pantalla y el cursor no tiene por donde salirse.
768
+ //
769
+ // Y avisarselo por un mouseMove sintetico no se puede: Chromium descarta los eventos
770
+ // cuyas coordenadas caen fuera de la ventana. Asi que el "esta afuera" lo resuelve el
771
+ // main, sin tocar lo que el renderer cree: cuando el cursor vuelve a entrar se reaplica
772
+ // su decision tal cual, y desde ahi los eventos reales —que si llegan con la ventana
773
+ // clickeable— lo ponen al dia.
774
+ // El "siempre encima" se vuelve a pedir en cada show, y no alcanza con pedirlo una vez al
775
+ // arrancar. Electron guarda el nivel de z-order como un estado suyo y solo baja a la API
776
+ // del sistema cuando ESE estado cambia: si Windows le saca el TOPMOST a la ventana por su
777
+ // cuenta, para Electron sigue estando arriba —isAlwaysOnTop() devuelve true— y no vuelve a
778
+ // pedirlo nunca. De ahi el false y despues el true: es la unica forma de forzar que se
779
+ // reemita.
780
+ //
781
+ // Antes esto no se notaba porque cada glass entraba con show(), que ademas de mostrar
782
+ // ACTIVA la ventana, y activarla la sube igual. Al pasar a showInactive() —para no robarle
783
+ // el teclado a lo que estes haciendo— se muestra sin activar, y con eso quedo a la vista
784
+ // que el alwaysOnTop no se estaba reponiendo.
785
+ //
786
+ // En Linux no se toca: ahi el "arriba de todo" es _NET_WM_STATE_ABOVE y quitarlo y ponerlo
787
+ // invita al gestor de ventanas a recolocar la ventana, que es justo lo que costo domar.
788
+ function reafirmarAlwaysOnTop() {
789
+ if (!mainWindow || mainWindow.isDestroyed()) return;
790
+ if (process.platform === 'linux') return;
791
+ mainWindow.setAlwaysOnTop(false);
792
+ mainWindow.setAlwaysOnTop(true, 'screen-saver');
793
+ }
794
+
795
+ function aplicarPassthrough({ forzar = false } = {}) {
796
+ if (!mainWindow || mainWindow.isDestroyed()) return;
797
+ const ignorar = passthroughPedidoPorRenderer || cursorFueraDeLaVentana;
798
+ if (ignorar === ignorandoMouse && !forzar) return;
799
+ ignorandoMouse = ignorar;
800
+ mainWindow.setIgnoreMouseEvents(ignorar, { forward: true });
801
+ }
802
+
803
+ function pedirPassthrough(ignore) {
804
+ passthroughPedidoPorRenderer = ignore;
805
+ aplicarPassthrough();
806
+ }
807
+
808
+ function iniciarSondeoCursor() {
809
+ if (sondeoCursor) return;
810
+ const enLinux = process.platform === 'linux';
811
+ sondeoCursor = setInterval(() => {
812
+ if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) return;
813
+
814
+ // En X11 screen.getCursorScreenPoint() no sirve: con la ventana ignorando el mouse
815
+ // Chromium se queda sin eventos y devuelve congelada la ultima posicion que vio
816
+ // (el porque, medido, esta en linuxCursor.js). Se le pregunta al servidor X. En
817
+ // Windows no hace falta: ahi la posicion sale del SO y viene siempre al dia.
818
+ const cursor = (enLinux ? cursorX11.posicion() : null) || screen.getCursorScreenPoint();
819
+ const area = mainWindow.getBounds();
820
+ const x = cursor.x - area.x;
821
+ const y = cursor.y - area.y;
822
+ const dentro = x >= 0 && y >= 0 && x < area.width && y < area.height;
823
+
824
+ if (!dentro !== cursorFueraDeLaVentana) {
825
+ cursorFueraDeLaVentana = !dentro;
826
+ aplicarPassthrough();
827
+ }
828
+ if (!dentro) return;
829
+
830
+ // De aca para abajo es solo de Linux. En Windows el `forward: true` de
831
+ // setIgnoreMouseEvents ya entrega los movimientos reales con la ventana ignorando
832
+ // el mouse —medido: el boton recibe mouseover y se puede clickear—, asi que
833
+ // inyectarlos seria duplicarlos.
834
+ if (!enLinux) return;
835
+
836
+ // Con el overlay ya clickeable los eventos reales llegan solos: inyectar seria
837
+ // duplicarlos.
838
+ if (!ignorandoMouse) return;
839
+
840
+ // Repetir el mismo punto en cada tick es ruido, pero callarse del todo mientras el
841
+ // cursor esta quieto rompe el otro caso: el menu se abre DEBAJO de un cursor que no
842
+ // se movio, no hay mousemove que lo cuente, y ese boton nace muerto. Se reemite cada
843
+ // tanto para que la UI que aparece sola se entere igual.
844
+ const ahora = Date.now();
845
+ const mismoPunto = x === ultimoPuntoEnviado.x && y === ultimoPuntoEnviado.y;
846
+ if (mismoPunto && ahora - ultimoEnvioMs < REEMISION_QUIETO_MS) return;
847
+ ultimoPuntoEnviado = { x, y };
848
+ ultimoEnvioMs = ahora;
849
+ mainWindow.webContents.sendInputEvent({ type: 'mouseMove', x, y });
850
+ }, SONDEO_CURSOR_MS);
851
+ }
852
+
853
+ function detenerSondeoCursor() {
854
+ cursorX11.detener();
855
+ if (!sondeoCursor) return;
856
+ clearInterval(sondeoCursor);
857
+ sondeoCursor = null;
858
+ ultimoPuntoEnviado = { x: null, y: null };
859
+ ultimoEnvioMs = 0;
860
+ cursorFueraDeLaVentana = false;
861
+ }
862
+
863
+ // Una ventana transparente en X11 necesita un compositor: sin el, el servidor X no sabe
864
+ // mezclar el ARGB con lo que hay detras y la ventana se pinta OPACA. En un overlay a
865
+ // pantalla completa eso no es un detalle estetico, es la pantalla entera tapada de blanco.
866
+ // La app no puede arreglarlo desde adentro —es una pieza del escritorio, no suya—, pero
867
+ // callarselo es peor: el equipo queda inservible y no hay una sola linea que lo explique.
868
+ //
869
+ // Solo se comprueba en XFCE, que es el unico escritorio comun que viene con el compositor
870
+ // apagable (y apagado en varias instalaciones). GNOME, KDE y Cinnamon componen siempre.
871
+ function avisarSiFaltaCompositor() {
872
+ if (process.platform !== 'linux') return;
873
+ if ((process.env.XDG_SESSION_TYPE || '') === 'wayland') return; // Wayland siempre compone
874
+ if (!(process.env.XDG_CURRENT_DESKTOP || '').toUpperCase().includes('XFCE')) return;
875
+
876
+ execFile('xfconf-query', ['-c', 'xfwm4', '-p', '/general/use_compositing'], (error, stdout) => {
877
+ if (error) return; // sin xfconf-query no hay nada que comprobar
878
+ if (stdout.trim() !== 'false') return;
879
+
880
+ const arreglo = 'xfconf-query -c xfwm4 -p /general/use_compositing -s true';
881
+ console.warn('[transparencia] XFCE tiene el compositor APAGADO: la ventana de glassnote se');
882
+ console.warn('[transparencia] dibuja opaca y tapa la pantalla. Se enciende con:');
883
+ console.warn(`[transparencia] ${arreglo}`);
884
+ console.warn('[transparencia] o en Ajustes del gestor de ventanas > Compositor.');
885
+
886
+ try {
887
+ new Notification({
888
+ title: 'GlassNote: falta el compositor',
889
+ body: `Sin compositor la ventana se ve opaca y tapa la pantalla. Encendelo con:\n${arreglo}`,
890
+ }).show();
891
+ } catch (error) {
892
+ /* un escritorio sin notificaciones no puede tumbar el arranque */
893
+ }
894
+ });
895
+ }
896
+
897
+ function cleanupAndQuit(motivo) {
510
898
  // Prevent multiple cleanup calls
511
899
  if (isCleaningUp) {
512
900
  return;
513
901
  }
514
902
  isCleaningUp = true;
903
+
904
+ // Cerrarse sin decir por que es lo que hacia imposible investigar los reinicios: en el
905
+ // log quedaba un silencio y despues el arranque siguiente, sin nada en el medio.
906
+ console.warn(`====== CIERRE VOLUNTARIO: ${motivo || 'sin motivo'} ======`);
907
+ marcarCierreLimpio(motivo || 'sin motivo');
515
908
 
516
909
 
517
910
  // Clear intervals and timeouts
911
+ detenerSondeoCursor();
518
912
  if (lockCheckerInterval) {
519
913
  clearInterval(lockCheckerInterval);
520
914
  lockCheckerInterval = null;
@@ -554,22 +948,22 @@ function cleanupAndQuit() {
554
948
 
555
949
  // Handle process signals for clean shutdown
556
950
  process.on('SIGINT', () => {
557
- cleanupAndQuit();
951
+ cleanupAndQuit('SIGINT');
558
952
  });
559
953
 
560
954
  process.on('SIGTERM', () => {
561
- cleanupAndQuit();
955
+ cleanupAndQuit('SIGTERM');
562
956
  });
563
957
 
564
958
  // Handle uncaught exceptions to prevent zombie processes
565
959
  process.on('uncaughtException', (error) => {
566
960
  console.error('Uncaught exception:', error);
567
- cleanupAndQuit();
961
+ cleanupAndQuit('excepcion no capturada');
568
962
  });
569
963
 
570
964
  process.on('unhandledRejection', (reason, promise) => {
571
965
  console.error('Unhandled promise rejection:', reason);
572
- cleanupAndQuit();
966
+ cleanupAndQuit('promesa rechazada sin manejar');
573
967
  });
574
968
 
575
969
  // Start lock checker after everything is initialized
@@ -577,143 +971,88 @@ process.on('unhandledRejection', (reason, promise) => {
577
971
 
578
972
 
579
973
  /**
580
- * Carga el HTML del renderer desde la fuente con versión SUPERIOR.
581
- *
582
- * Dos fuentes, y la division importa:
583
- * - userData: lo que bajo el auto-update de S3. Es el canal EN CALIENTE: una version
584
- * nueva del renderer llega ahi sin republicar el cliente ni tocar el binario.
585
- * - baseline: el renderer que viaja con la instalacion. Es lo que ve una instalacion
586
- * nueva antes de su primer update en caliente.
587
- *
588
- * Gana la version mas alta, sin importar de donde venga; empate, gana userData.
974
+ * Carga el HTML del renderer. Cual de las dos fuentes gana lo decide resolverRenderer()
975
+ * en rendererPaths.js, que es tambien de donde sale la version que muestra el menu.
976
+ */
977
+ /**
978
+ * Abre el ConfigMenu. Es lo unico de la app que usa el teclado, y desde que los glasses
979
+ * se muestran con showInactive() la ventana ya no viene enfocada de arriba: hay que
980
+ * mostrarla y pedirle el foco a mano antes de avisarle al renderer.
589
981
  */
590
- // baselineRendererDir vive en rendererPaths.js: lo comparte con el updater del renderer.
982
+ function abrirConfigMenu(vista) {
983
+ if (!mainWindow || mainWindow.isDestroyed()) return;
984
+ mainWindow.show();
985
+ mainWindow.focus();
986
+ mainWindow.webContents.send('show-config-menu', vista || 'review');
987
+ }
591
988
 
592
989
  function loadRendererHtml(window, uuid) {
593
- // Definir las dos fuentes a considerar con archivos HTML alternativos
594
- const sources = [
595
- {
596
- name: 'userData',
597
- dir: path.join(userData.getUserDataDir(), 'renderer'),
598
- htmlFiles: ['glassnote.html'], // userData siempre usa glassnote.html
599
- type: 'auto-update'
600
- },
601
- {
602
- name: 'baseline',
603
- dir: baselineRendererDir(),
604
- htmlFiles: ['glassnote.html', 'index.html'], // Intenta glassnote.html primero, luego index.html
605
- type: 'paquete'
606
- }
607
- ];
608
-
609
-
610
- // Obtener versiones y verificar archivos HTML para ambas fuentes
611
- const sourcesWithInfo = sources.map(source => {
612
- const version = getRendererVersionFromPath(source.dir);
613
-
614
- // Buscar el primer archivo HTML que exista
615
- let htmlPath = null;
616
- let htmlFile = null;
617
- for (const file of source.htmlFiles) {
618
- const potentialPath = path.join(source.dir, file);
619
- if (fs.existsSync(potentialPath)) {
620
- htmlPath = potentialPath;
621
- htmlFile = file;
622
- break;
623
- }
624
- }
625
-
626
- const htmlExists = htmlPath !== null;
627
-
628
- return {
629
- ...source,
630
- version,
631
- htmlPath,
632
- htmlFile,
633
- htmlExists
634
- };
635
- });
636
-
637
- // Log de información encontrada
638
- sourcesWithInfo.forEach(source => {
639
- if (source.htmlExists) {
640
- } else {
641
- console.log(` ${source.name}: versión ${source.version}, SIN ARCHIVO HTML`);
990
+ const { elegida, fuentes } = resolverRenderer();
991
+
992
+ for (const fuente of fuentes) {
993
+ if (!fuente.htmlPath) {
994
+ console.log(` ${fuente.nombre}: versión ${fuente.version}, SIN ARCHIVO HTML`);
642
995
  }
643
- });
644
-
645
- // Comparar versiones para determinar cuál es superior
646
- const versionComparison = compareVersions(
647
- sourcesWithInfo[0].version,
648
- sourcesWithInfo[1].version
649
- );
650
-
651
- // Determinar fuente con versión superior
652
- let superiorSource;
653
- if (versionComparison > 0) {
654
- // userData tiene versión superior
655
- superiorSource = sourcesWithInfo[0];
656
- } else if (versionComparison < 0) {
657
- // glassnote-renderer/dist tiene versión superior
658
- superiorSource = sourcesWithInfo[1];
659
- } else {
660
- // Versiones iguales, priorizar userData (auto-update)
661
- superiorSource = sourcesWithInfo[0];
662
996
  }
663
997
 
664
- // Intentar cargar desde la fuente con versión superior
665
- if (superiorSource.htmlExists) {
666
- window.loadFile(superiorSource.htmlPath, {
998
+ if (elegida) {
999
+ window.loadFile(elegida.htmlPath, {
667
1000
  query: { uuid: uuid, version: packageJson.version }
668
1001
  });
1002
+ // El menu de la bandeja muestra la version del renderer, y esta es la linea que la
1003
+ // decide: rearmarlo aca es lo que hace que despues de un update en caliente el numero
1004
+ // del menu sea el que se acaba de cargar y no el del arranque.
1005
+ actualizarMenuTray();
669
1006
  return;
670
1007
  }
671
1008
 
672
- // Si la fuente con versión superior no tiene HTML, intentar la otra fuente
673
- const otherSource = superiorSource.name === 'userData' ? sourcesWithInfo[1] : sourcesWithInfo[0];
674
-
675
- if (otherSource.htmlExists) {
676
-
677
- window.loadFile(otherSource.htmlPath, {
678
- query: { uuid: uuid, version: packageJson.version }
679
- });
680
- return;
1009
+ console.error('ERROR: No se encontró archivo HTML del renderer en ninguna fuente');
1010
+ for (const fuente of fuentes) {
1011
+ console.error(`${fuente.nombre} (${fuente.dir}) buscó:`, fuente.htmls.join(', '));
681
1012
  }
682
1013
 
683
- // Si ninguna fuente tiene HTML, error
684
- console.error('ERROR: No se encontró archivo HTML del renderer en ninguna fuente');
685
- console.error('userData buscó:', sourcesWithInfo[0].htmlFiles.join(', '));
686
- console.error(`baseline (${sourcesWithInfo[1].dir}) buscó:`, sourcesWithInfo[1].htmlFiles.join(', '));
687
-
688
1014
  // Error fatal - no hay renderer disponible
689
1015
  console.error('ERROR FATAL: No se puede cargar el renderer');
690
- // Intentar cargar una página de error o cerrar la aplicación
691
1016
  window.loadURL('data:text/html,<h1>Error: No se pudo cargar el renderer</h1>');
692
1017
  }
693
1018
 
694
1019
  /**
695
- * Compara dos versiones semánticas (formato: major.minor.patch)
696
- * Retorna 1 si version1 > version2
697
- * Retorna 0 si version1 == version2
698
- * Retorna -1 si version1 < version2
1020
+ * El menu de la bandeja. Se rearma entero en vez de editarse porque Electron no deja
1021
+ * cambiar la etiqueta de un item ya construido en Linux.
1022
+ *
1023
+ * Lleva las dos versiones a proposito: el cliente y el renderer se actualizan por vias
1024
+ * distintas —npm una, el zip de S3 la otra— y se desincronizan seguido. Con un solo
1025
+ * numero a la vista no habia forma de saber, mirando el equipo, cual de las dos partes
1026
+ * era la vieja.
699
1027
  */
700
- function compareVersions(version1, version2) {
701
- const v1Parts = version1.split('.').map((part) => parseInt(part) || 0);
702
- const v2Parts = version2.split('.').map((part) => parseInt(part) || 0);
703
-
704
- // Comparar partes principales (major, minor, patch)
705
- for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
706
- const v1Part = v1Parts[i] || 0;
707
- const v2Part = v2Parts[i] || 0;
708
-
709
- if (v1Part > v2Part) return 1;
710
- if (v1Part < v2Part) return -1;
711
- }
712
-
713
- return 0; // Las versiones son iguales
1028
+ function construirMenuTray(versionRenderer) {
1029
+ return Menu.buildFromTemplate([
1030
+ {
1031
+ label: `glassnote v${packageJson.version}`,
1032
+ enabled: false,
1033
+ },
1034
+ {
1035
+ label: versionRenderer ? `renderer v${versionRenderer}` : 'renderer: no cargado',
1036
+ enabled: false,
1037
+ },
1038
+ { type: 'separator' },
1039
+ {
1040
+ label: 'Config Menu',
1041
+ click: () => {
1042
+ abrirConfigMenu('review');
1043
+ }
1044
+ }
1045
+ ]);
714
1046
  }
715
1047
 
716
- // getRendererVersionFromPath = rendererVersionFrom de rendererPaths.js (ver el require de arriba).
1048
+ function actualizarMenuTray() {
1049
+ if (!tray || tray.isDestroyed()) return;
1050
+ const versionRenderer = (resolverRenderer().elegida || {}).version;
1051
+ tray.setToolTip(
1052
+ `GlassNote v${packageJson.version}` + (versionRenderer ? ` · renderer v${versionRenderer}` : '')
1053
+ );
1054
+ tray.setContextMenu(construirMenuTray(versionRenderer));
1055
+ }
717
1056
 
718
1057
  /**
719
1058
  * Recarga el main window después de una actualización del renderer
@@ -726,6 +1065,12 @@ function reloadMainWindow() {
726
1065
  localServer.updateMainWindow(mainWindow);
727
1066
  }
728
1067
 
1068
+ // El renderer que vuelve a cargar arranca creyendo que el overlay deja pasar el
1069
+ // mouse. Si habia quedado clickeable, nadie se lo iba a devolver: el renderer solo
1070
+ // avisa cuando CAMBIA de estado, y para el recien nacido no cambio nada. Se vuelve
1071
+ // al estado inicial a mano para que las dos mitades arranquen de acuerdo.
1072
+ pedirPassthrough(true);
1073
+
729
1074
  loadRendererHtml(mainWindow, uuid);
730
1075
  }
731
1076
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassnote/client",
3
- "version": "2.4.11",
3
+ "version": "2.4.13",
4
4
  "description": "GlassNote — cliente de escritorio de notas superpuestas. Se instala con npx, sin instaladores nativos ni firmas.",
5
5
  "main": "main.js",
6
6
  "publishConfig": {
@@ -30,6 +30,7 @@
30
30
  "autoupdate.js",
31
31
  "autoupdate_renderer.js",
32
32
  "rendererPaths.js",
33
+ "linuxCursor.js",
33
34
  "npmMode.js",
34
35
  "npmUpdate.js",
35
36
  "versionReport.js",
@@ -73,7 +74,7 @@
73
74
  "url": "git+ssh://git@intermarkec/intermarkec/glassnote-electron.git"
74
75
  },
75
76
  "dependencies": {
76
- "@glassnote/renderer": "2.0.9",
77
+ "@glassnote/renderer": "2.0.12",
77
78
  "cors": "^2.8.5",
78
79
  "electron": "^29.4.6",
79
80
  "express": "^4.18.2",
package/rendererPaths.js CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
+ const userData = require('./userData');
13
14
 
14
15
  // El baseline es la dependencia @glassnote/renderer, que npm resuelve sola. Pero si hay
15
16
  // un clon del renderer al lado —el flujo de quien lo desarrolla, con `npm run
@@ -59,4 +60,59 @@ function compareRendererVersions(a, b) {
59
60
  return 0;
60
61
  }
61
62
 
62
- module.exports = { baselineRendererDir, rendererVersionFrom, compareRendererVersions };
63
+ // Cual de los dos renderers se usa de verdad, con el mismo criterio para el que lo carga
64
+ // y para el que solo quiere mostrar la version. Si fueran dos calculos, el numero del menu
65
+ // terminaria mintiendo el dia que uno de los dos cambie.
66
+ //
67
+ // - userData: lo que bajo el auto-update de S3. Es el canal EN CALIENTE: una version
68
+ // nueva llega ahi sin republicar el cliente ni tocar el binario.
69
+ // - baseline: el renderer que viaja con la instalacion, lo que ve un equipo recien
70
+ // instalado antes de su primer update en caliente.
71
+ //
72
+ // Gana la version mas alta venga de donde venga; si empatan gana userData. Pero una
73
+ // fuente sin HTML no se puede cargar, asi que en ese caso se cae a la otra: la version
74
+ // que se informa es la que de verdad esta corriendo, no la que gano la comparacion.
75
+ function resolverRenderer() {
76
+ const fuentes = [
77
+ {
78
+ nombre: 'userData',
79
+ dir: path.join(userData.getUserDataDir(), 'renderer'),
80
+ htmls: ['glassnote.html'], // userData siempre usa glassnote.html
81
+ },
82
+ {
83
+ nombre: 'baseline',
84
+ dir: baselineRendererDir(),
85
+ htmls: ['glassnote.html', 'index.html'],
86
+ },
87
+ ].map((fuente) => {
88
+ let htmlPath = null;
89
+ for (const archivo of fuente.htmls) {
90
+ const posible = path.join(fuente.dir, archivo);
91
+ if (fs.existsSync(posible)) {
92
+ htmlPath = posible;
93
+ break;
94
+ }
95
+ }
96
+ return { ...fuente, version: rendererVersionFrom(fuente.dir), htmlPath };
97
+ });
98
+
99
+ const [enCaliente, baseline] = fuentes;
100
+ const gana = compareRendererVersions(enCaliente.version, baseline.version) < 0 ? baseline : enCaliente;
101
+ const otra = gana === enCaliente ? baseline : enCaliente;
102
+
103
+ return { elegida: gana.htmlPath ? gana : otra.htmlPath ? otra : null, fuentes };
104
+ }
105
+
106
+ // La version del renderer que esta corriendo, o null si no hay ninguno cargable.
107
+ function rendererVersionActiva() {
108
+ const { elegida } = resolverRenderer();
109
+ return elegida ? elegida.version : null;
110
+ }
111
+
112
+ module.exports = {
113
+ baselineRendererDir,
114
+ rendererVersionFrom,
115
+ compareRendererVersions,
116
+ resolverRenderer,
117
+ rendererVersionActiva,
118
+ };