@glassnote/client 2.4.11 → 2.4.12
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 +187 -0
- package/main.js +476 -153
- package/package.json +3 -2
- package/rendererPaths.js +57 -1
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 {
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,20 +487,87 @@ 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
|
-
|
|
494
|
+
pedirPassthrough(true);
|
|
365
495
|
});
|
|
366
496
|
ipcMain.on('set-ignore-events-false', () => {
|
|
367
|
-
|
|
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.
|
|
508
|
+
mainWindow.showInactive();
|
|
509
|
+
}
|
|
510
|
+
});
|
|
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();
|
|
374
563
|
}
|
|
564
|
+
aplicarPassthrough({ forzar: true });
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// El canal ya estaba habilitado en el preload para renderer -> main, pero del lado de
|
|
568
|
+
// main no lo escuchaba nadie: el menu solo se podia abrir desde la bandeja.
|
|
569
|
+
ipcMain.on('show-config-menu', (event, vista) => {
|
|
570
|
+
abrirConfigMenu(vista);
|
|
375
571
|
});
|
|
376
572
|
|
|
377
573
|
ipcMain.on('hide-window', () => {
|
|
@@ -404,6 +600,9 @@ app.whenReady().then(() => {
|
|
|
404
600
|
const registrationWindow = new BrowserWindow({
|
|
405
601
|
width: 1200,
|
|
406
602
|
height: 800,
|
|
603
|
+
// Sin esto Electron no pone _NET_WM_ICON y la unica ventana que sale en la barra de
|
|
604
|
+
// tareas de Linux aparece con el icono generico del gestor de ventanas.
|
|
605
|
+
icon: path.join(__dirname, 'images/icon.png'),
|
|
407
606
|
minWidth: 800,
|
|
408
607
|
minHeight: 600,
|
|
409
608
|
show: false,
|
|
@@ -434,7 +633,7 @@ app.whenReady().then(() => {
|
|
|
434
633
|
});
|
|
435
634
|
|
|
436
635
|
mainWindow.on('ready-to-show', () => {
|
|
437
|
-
mainWindow.
|
|
636
|
+
mainWindow.showInactive();
|
|
438
637
|
|
|
439
638
|
// Use LocalServer singleton
|
|
440
639
|
if (!localServer) {
|
|
@@ -449,12 +648,30 @@ app.whenReady().then(() => {
|
|
|
449
648
|
lockChecker();
|
|
450
649
|
});
|
|
451
650
|
|
|
651
|
+
// Un crash del renderer o de un hijo (GPU, utility) no pasa por ningun handler de los de
|
|
652
|
+
// arriba: no es una excepcion de JavaScript ni una senal, asi que el log se quedaba mudo.
|
|
653
|
+
// `reason` es lo que hay que leer: 'crashed', 'oom', 'killed', 'abnormal-exit',
|
|
654
|
+
// 'launch-failed', 'integrity-failure' o 'clean-exit'.
|
|
655
|
+
app.on('render-process-gone', (event, webContents, details) => {
|
|
656
|
+
console.error(
|
|
657
|
+
`====== MURIO EL RENDERER: ${details.reason} (exitCode ${details.exitCode}) ======`
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
app.on('child-process-gone', (event, details) => {
|
|
662
|
+
console.error(
|
|
663
|
+
`====== MURIO UN PROCESO HIJO: ${details.type}` +
|
|
664
|
+
`${details.name ? ' (' + details.name + ')' : ''} ` +
|
|
665
|
+
`${details.reason} (exitCode ${details.exitCode}) ======`
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
|
|
452
669
|
app.on('window-all-closed', () => {
|
|
453
670
|
// Don't quit when all windows are closed if we have a tray
|
|
454
671
|
if (tray && process.platform !== 'darwin') {
|
|
455
672
|
// Keep the app running with tray
|
|
456
673
|
} else {
|
|
457
|
-
cleanupAndQuit();
|
|
674
|
+
cleanupAndQuit('se cerraron todas las ventanas');
|
|
458
675
|
}
|
|
459
676
|
});
|
|
460
677
|
|
|
@@ -498,23 +715,178 @@ function lockChecker() {
|
|
|
498
715
|
lockCheckerInterval = setInterval(() => {
|
|
499
716
|
// Leer el lock directamente del archivo, no del cache
|
|
500
717
|
const currentData = userData.getLockFromFile();
|
|
501
|
-
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
718
|
+
|
|
719
|
+
// Esta es la via por la que la app se cerraba sin dejar rastro, y las dos causas no
|
|
720
|
+
// son la misma cosa: que el archivo no este es un problema del disco o de algo que
|
|
721
|
+
// lo borro, y que el lock haya cambiado significa que arranco otra instancia y esta
|
|
722
|
+
// le cede el lugar. Se distinguen en el log porque llevan a sitios muy distintos.
|
|
723
|
+
if (currentData === undefined) {
|
|
724
|
+
cleanupAndQuit('el lock no se pudo leer: .glassnote no esta o no tiene lock');
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (currentData.toString() != initialLock.toString()) {
|
|
728
|
+
cleanupAndQuit(
|
|
729
|
+
`el lock cambio (mio ${initialLock}, en el archivo ${currentData}): arranco otra instancia`
|
|
730
|
+
);
|
|
505
731
|
}
|
|
506
732
|
}, 10000); // Check every 10 seconds
|
|
507
733
|
}
|
|
508
734
|
|
|
509
|
-
|
|
735
|
+
// El overlay deja pasar el mouse y solo se vuelve clickeable cuando el cursor esta encima
|
|
736
|
+
// de algo interactivo. Quien decide eso es el renderer: escucha `mousemove` en el document,
|
|
737
|
+
// compara la posicion contra los rectangulos de los elementos que registro, y pide apagar o
|
|
738
|
+
// encender el passthrough. Para decidir, entonces, necesita ver el mouse.
|
|
739
|
+
//
|
|
740
|
+
// En Windows y macOS se lo da `forward: true`. En Linux esa opcion NO EXISTE: Electron le
|
|
741
|
+
// pone al window una input shape de 1x1 px en X11 y el flag ni se mira (SetIgnoreMouseEvents
|
|
742
|
+
// en native_window_views.cc solo lo usa bajo IS_WIN). Sin un solo evento de mouse la
|
|
743
|
+
// posicion que guarda el renderer se queda en null para siempre, ningun elemento resulta
|
|
744
|
+
// nunca "bajo el cursor", y por eso en Linux no responde un boton.
|
|
745
|
+
//
|
|
746
|
+
// Se emula: se lee la posicion global del cursor y se le inyecta al renderer un mouseMove
|
|
747
|
+
// sintetico, que es exactamente lo que forwardea Windows. El hit-test sigue donde estan las
|
|
748
|
+
// zonas —en el renderer—; desde aca solo se le cuenta donde esta el mouse.
|
|
749
|
+
const SONDEO_CURSOR_MS = 30;
|
|
750
|
+
// Cada cuanto se repite la posicion aunque el cursor no se haya movido.
|
|
751
|
+
const REEMISION_QUIETO_MS = 400;
|
|
752
|
+
|
|
753
|
+
// El passthrough efectivo sale de dos cosas: lo que pidio el renderer y si el cursor esta
|
|
754
|
+
// siquiera sobre la ventana.
|
|
755
|
+
//
|
|
756
|
+
// Lo segundo no es un detalle: cuando el overlay esta clickeable y el cursor se va —a otro
|
|
757
|
+
// monitor, por ejemplo— el renderer no recibe un mousemove mas, se queda con la ultima
|
|
758
|
+
// posicion (encima del boton) y nunca devuelve el passthrough. El overlay se queda comiendo
|
|
759
|
+
// TODOS los clics de esa pantalla.
|
|
760
|
+
//
|
|
761
|
+
// No es solo de X11. Medido en Windows con dos monitores: al salir la ventana NO llega
|
|
762
|
+
// ningun mouseleave —se probo, y el evento recien aparece cuando el cursor vuelve—, asi que
|
|
763
|
+
// el overlay queda clickeable todo el tiempo que estes en la otra pantalla. Ahi se cura
|
|
764
|
+
// solo al volver, porque con la ventana clickeable los movimientos reales si le llegan;
|
|
765
|
+
// en Linux quedaba trabado hasta reiniciar. Con un solo monitor no se dispara: el overlay
|
|
766
|
+
// ocupa toda la pantalla y el cursor no tiene por donde salirse.
|
|
767
|
+
//
|
|
768
|
+
// Y avisarselo por un mouseMove sintetico no se puede: Chromium descarta los eventos
|
|
769
|
+
// cuyas coordenadas caen fuera de la ventana. Asi que el "esta afuera" lo resuelve el
|
|
770
|
+
// main, sin tocar lo que el renderer cree: cuando el cursor vuelve a entrar se reaplica
|
|
771
|
+
// su decision tal cual, y desde ahi los eventos reales —que si llegan con la ventana
|
|
772
|
+
// clickeable— lo ponen al dia.
|
|
773
|
+
function aplicarPassthrough({ forzar = false } = {}) {
|
|
774
|
+
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
775
|
+
const ignorar = passthroughPedidoPorRenderer || cursorFueraDeLaVentana;
|
|
776
|
+
if (ignorar === ignorandoMouse && !forzar) return;
|
|
777
|
+
ignorandoMouse = ignorar;
|
|
778
|
+
mainWindow.setIgnoreMouseEvents(ignorar, { forward: true });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function pedirPassthrough(ignore) {
|
|
782
|
+
passthroughPedidoPorRenderer = ignore;
|
|
783
|
+
aplicarPassthrough();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function iniciarSondeoCursor() {
|
|
787
|
+
if (sondeoCursor) return;
|
|
788
|
+
const enLinux = process.platform === 'linux';
|
|
789
|
+
sondeoCursor = setInterval(() => {
|
|
790
|
+
if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) return;
|
|
791
|
+
|
|
792
|
+
// En X11 screen.getCursorScreenPoint() no sirve: con la ventana ignorando el mouse
|
|
793
|
+
// Chromium se queda sin eventos y devuelve congelada la ultima posicion que vio
|
|
794
|
+
// (el porque, medido, esta en linuxCursor.js). Se le pregunta al servidor X. En
|
|
795
|
+
// Windows no hace falta: ahi la posicion sale del SO y viene siempre al dia.
|
|
796
|
+
const cursor = (enLinux ? cursorX11.posicion() : null) || screen.getCursorScreenPoint();
|
|
797
|
+
const area = mainWindow.getBounds();
|
|
798
|
+
const x = cursor.x - area.x;
|
|
799
|
+
const y = cursor.y - area.y;
|
|
800
|
+
const dentro = x >= 0 && y >= 0 && x < area.width && y < area.height;
|
|
801
|
+
|
|
802
|
+
if (!dentro !== cursorFueraDeLaVentana) {
|
|
803
|
+
cursorFueraDeLaVentana = !dentro;
|
|
804
|
+
aplicarPassthrough();
|
|
805
|
+
}
|
|
806
|
+
if (!dentro) return;
|
|
807
|
+
|
|
808
|
+
// De aca para abajo es solo de Linux. En Windows el `forward: true` de
|
|
809
|
+
// setIgnoreMouseEvents ya entrega los movimientos reales con la ventana ignorando
|
|
810
|
+
// el mouse —medido: el boton recibe mouseover y se puede clickear—, asi que
|
|
811
|
+
// inyectarlos seria duplicarlos.
|
|
812
|
+
if (!enLinux) return;
|
|
813
|
+
|
|
814
|
+
// Con el overlay ya clickeable los eventos reales llegan solos: inyectar seria
|
|
815
|
+
// duplicarlos.
|
|
816
|
+
if (!ignorandoMouse) return;
|
|
817
|
+
|
|
818
|
+
// Repetir el mismo punto en cada tick es ruido, pero callarse del todo mientras el
|
|
819
|
+
// cursor esta quieto rompe el otro caso: el menu se abre DEBAJO de un cursor que no
|
|
820
|
+
// se movio, no hay mousemove que lo cuente, y ese boton nace muerto. Se reemite cada
|
|
821
|
+
// tanto para que la UI que aparece sola se entere igual.
|
|
822
|
+
const ahora = Date.now();
|
|
823
|
+
const mismoPunto = x === ultimoPuntoEnviado.x && y === ultimoPuntoEnviado.y;
|
|
824
|
+
if (mismoPunto && ahora - ultimoEnvioMs < REEMISION_QUIETO_MS) return;
|
|
825
|
+
ultimoPuntoEnviado = { x, y };
|
|
826
|
+
ultimoEnvioMs = ahora;
|
|
827
|
+
mainWindow.webContents.sendInputEvent({ type: 'mouseMove', x, y });
|
|
828
|
+
}, SONDEO_CURSOR_MS);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function detenerSondeoCursor() {
|
|
832
|
+
cursorX11.detener();
|
|
833
|
+
if (!sondeoCursor) return;
|
|
834
|
+
clearInterval(sondeoCursor);
|
|
835
|
+
sondeoCursor = null;
|
|
836
|
+
ultimoPuntoEnviado = { x: null, y: null };
|
|
837
|
+
ultimoEnvioMs = 0;
|
|
838
|
+
cursorFueraDeLaVentana = false;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Una ventana transparente en X11 necesita un compositor: sin el, el servidor X no sabe
|
|
842
|
+
// mezclar el ARGB con lo que hay detras y la ventana se pinta OPACA. En un overlay a
|
|
843
|
+
// pantalla completa eso no es un detalle estetico, es la pantalla entera tapada de blanco.
|
|
844
|
+
// La app no puede arreglarlo desde adentro —es una pieza del escritorio, no suya—, pero
|
|
845
|
+
// callarselo es peor: el equipo queda inservible y no hay una sola linea que lo explique.
|
|
846
|
+
//
|
|
847
|
+
// Solo se comprueba en XFCE, que es el unico escritorio comun que viene con el compositor
|
|
848
|
+
// apagable (y apagado en varias instalaciones). GNOME, KDE y Cinnamon componen siempre.
|
|
849
|
+
function avisarSiFaltaCompositor() {
|
|
850
|
+
if (process.platform !== 'linux') return;
|
|
851
|
+
if ((process.env.XDG_SESSION_TYPE || '') === 'wayland') return; // Wayland siempre compone
|
|
852
|
+
if (!(process.env.XDG_CURRENT_DESKTOP || '').toUpperCase().includes('XFCE')) return;
|
|
853
|
+
|
|
854
|
+
execFile('xfconf-query', ['-c', 'xfwm4', '-p', '/general/use_compositing'], (error, stdout) => {
|
|
855
|
+
if (error) return; // sin xfconf-query no hay nada que comprobar
|
|
856
|
+
if (stdout.trim() !== 'false') return;
|
|
857
|
+
|
|
858
|
+
const arreglo = 'xfconf-query -c xfwm4 -p /general/use_compositing -s true';
|
|
859
|
+
console.warn('[transparencia] XFCE tiene el compositor APAGADO: la ventana de glassnote se');
|
|
860
|
+
console.warn('[transparencia] dibuja opaca y tapa la pantalla. Se enciende con:');
|
|
861
|
+
console.warn(`[transparencia] ${arreglo}`);
|
|
862
|
+
console.warn('[transparencia] o en Ajustes del gestor de ventanas > Compositor.');
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
new Notification({
|
|
866
|
+
title: 'GlassNote: falta el compositor',
|
|
867
|
+
body: `Sin compositor la ventana se ve opaca y tapa la pantalla. Encendelo con:\n${arreglo}`,
|
|
868
|
+
}).show();
|
|
869
|
+
} catch (error) {
|
|
870
|
+
/* un escritorio sin notificaciones no puede tumbar el arranque */
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function cleanupAndQuit(motivo) {
|
|
510
876
|
// Prevent multiple cleanup calls
|
|
511
877
|
if (isCleaningUp) {
|
|
512
878
|
return;
|
|
513
879
|
}
|
|
514
880
|
isCleaningUp = true;
|
|
881
|
+
|
|
882
|
+
// Cerrarse sin decir por que es lo que hacia imposible investigar los reinicios: en el
|
|
883
|
+
// log quedaba un silencio y despues el arranque siguiente, sin nada en el medio.
|
|
884
|
+
console.warn(`====== CIERRE VOLUNTARIO: ${motivo || 'sin motivo'} ======`);
|
|
885
|
+
marcarCierreLimpio(motivo || 'sin motivo');
|
|
515
886
|
|
|
516
887
|
|
|
517
888
|
// Clear intervals and timeouts
|
|
889
|
+
detenerSondeoCursor();
|
|
518
890
|
if (lockCheckerInterval) {
|
|
519
891
|
clearInterval(lockCheckerInterval);
|
|
520
892
|
lockCheckerInterval = null;
|
|
@@ -554,22 +926,22 @@ function cleanupAndQuit() {
|
|
|
554
926
|
|
|
555
927
|
// Handle process signals for clean shutdown
|
|
556
928
|
process.on('SIGINT', () => {
|
|
557
|
-
cleanupAndQuit();
|
|
929
|
+
cleanupAndQuit('SIGINT');
|
|
558
930
|
});
|
|
559
931
|
|
|
560
932
|
process.on('SIGTERM', () => {
|
|
561
|
-
cleanupAndQuit();
|
|
933
|
+
cleanupAndQuit('SIGTERM');
|
|
562
934
|
});
|
|
563
935
|
|
|
564
936
|
// Handle uncaught exceptions to prevent zombie processes
|
|
565
937
|
process.on('uncaughtException', (error) => {
|
|
566
938
|
console.error('Uncaught exception:', error);
|
|
567
|
-
cleanupAndQuit();
|
|
939
|
+
cleanupAndQuit('excepcion no capturada');
|
|
568
940
|
});
|
|
569
941
|
|
|
570
942
|
process.on('unhandledRejection', (reason, promise) => {
|
|
571
943
|
console.error('Unhandled promise rejection:', reason);
|
|
572
|
-
cleanupAndQuit();
|
|
944
|
+
cleanupAndQuit('promesa rechazada sin manejar');
|
|
573
945
|
});
|
|
574
946
|
|
|
575
947
|
// Start lock checker after everything is initialized
|
|
@@ -577,143 +949,88 @@ process.on('unhandledRejection', (reason, promise) => {
|
|
|
577
949
|
|
|
578
950
|
|
|
579
951
|
/**
|
|
580
|
-
* Carga el HTML del renderer
|
|
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.
|
|
952
|
+
* Carga el HTML del renderer. Cual de las dos fuentes gana lo decide resolverRenderer()
|
|
953
|
+
* en rendererPaths.js, que es tambien de donde sale la version que muestra el menu.
|
|
589
954
|
*/
|
|
590
|
-
|
|
955
|
+
/**
|
|
956
|
+
* Abre el ConfigMenu. Es lo unico de la app que usa el teclado, y desde que los glasses
|
|
957
|
+
* se muestran con showInactive() la ventana ya no viene enfocada de arriba: hay que
|
|
958
|
+
* mostrarla y pedirle el foco a mano antes de avisarle al renderer.
|
|
959
|
+
*/
|
|
960
|
+
function abrirConfigMenu(vista) {
|
|
961
|
+
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
962
|
+
mainWindow.show();
|
|
963
|
+
mainWindow.focus();
|
|
964
|
+
mainWindow.webContents.send('show-config-menu', vista || 'review');
|
|
965
|
+
}
|
|
591
966
|
|
|
592
967
|
function loadRendererHtml(window, uuid) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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`);
|
|
968
|
+
const { elegida, fuentes } = resolverRenderer();
|
|
969
|
+
|
|
970
|
+
for (const fuente of fuentes) {
|
|
971
|
+
if (!fuente.htmlPath) {
|
|
972
|
+
console.log(` ${fuente.nombre}: versión ${fuente.version}, SIN ARCHIVO HTML`);
|
|
642
973
|
}
|
|
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
974
|
}
|
|
663
975
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
window.loadFile(superiorSource.htmlPath, {
|
|
976
|
+
if (elegida) {
|
|
977
|
+
window.loadFile(elegida.htmlPath, {
|
|
667
978
|
query: { uuid: uuid, version: packageJson.version }
|
|
668
979
|
});
|
|
980
|
+
// El menu de la bandeja muestra la version del renderer, y esta es la linea que la
|
|
981
|
+
// decide: rearmarlo aca es lo que hace que despues de un update en caliente el numero
|
|
982
|
+
// del menu sea el que se acaba de cargar y no el del arranque.
|
|
983
|
+
actualizarMenuTray();
|
|
669
984
|
return;
|
|
670
985
|
}
|
|
671
986
|
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
if (otherSource.htmlExists) {
|
|
676
|
-
|
|
677
|
-
window.loadFile(otherSource.htmlPath, {
|
|
678
|
-
query: { uuid: uuid, version: packageJson.version }
|
|
679
|
-
});
|
|
680
|
-
return;
|
|
987
|
+
console.error('ERROR: No se encontró archivo HTML del renderer en ninguna fuente');
|
|
988
|
+
for (const fuente of fuentes) {
|
|
989
|
+
console.error(`${fuente.nombre} (${fuente.dir}) buscó:`, fuente.htmls.join(', '));
|
|
681
990
|
}
|
|
682
991
|
|
|
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
992
|
// Error fatal - no hay renderer disponible
|
|
689
993
|
console.error('ERROR FATAL: No se puede cargar el renderer');
|
|
690
|
-
// Intentar cargar una página de error o cerrar la aplicación
|
|
691
994
|
window.loadURL('data:text/html,<h1>Error: No se pudo cargar el renderer</h1>');
|
|
692
995
|
}
|
|
693
996
|
|
|
694
997
|
/**
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
998
|
+
* El menu de la bandeja. Se rearma entero en vez de editarse porque Electron no deja
|
|
999
|
+
* cambiar la etiqueta de un item ya construido en Linux.
|
|
1000
|
+
*
|
|
1001
|
+
* Lleva las dos versiones a proposito: el cliente y el renderer se actualizan por vias
|
|
1002
|
+
* distintas —npm una, el zip de S3 la otra— y se desincronizan seguido. Con un solo
|
|
1003
|
+
* numero a la vista no habia forma de saber, mirando el equipo, cual de las dos partes
|
|
1004
|
+
* era la vieja.
|
|
699
1005
|
*/
|
|
700
|
-
function
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1006
|
+
function construirMenuTray(versionRenderer) {
|
|
1007
|
+
return Menu.buildFromTemplate([
|
|
1008
|
+
{
|
|
1009
|
+
label: `glassnote v${packageJson.version}`,
|
|
1010
|
+
enabled: false,
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
label: versionRenderer ? `renderer v${versionRenderer}` : 'renderer: no cargado',
|
|
1014
|
+
enabled: false,
|
|
1015
|
+
},
|
|
1016
|
+
{ type: 'separator' },
|
|
1017
|
+
{
|
|
1018
|
+
label: 'Config Menu',
|
|
1019
|
+
click: () => {
|
|
1020
|
+
abrirConfigMenu('review');
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
]);
|
|
714
1024
|
}
|
|
715
1025
|
|
|
716
|
-
|
|
1026
|
+
function actualizarMenuTray() {
|
|
1027
|
+
if (!tray || tray.isDestroyed()) return;
|
|
1028
|
+
const versionRenderer = (resolverRenderer().elegida || {}).version;
|
|
1029
|
+
tray.setToolTip(
|
|
1030
|
+
`GlassNote v${packageJson.version}` + (versionRenderer ? ` · renderer v${versionRenderer}` : '')
|
|
1031
|
+
);
|
|
1032
|
+
tray.setContextMenu(construirMenuTray(versionRenderer));
|
|
1033
|
+
}
|
|
717
1034
|
|
|
718
1035
|
/**
|
|
719
1036
|
* Recarga el main window después de una actualización del renderer
|
|
@@ -726,6 +1043,12 @@ function reloadMainWindow() {
|
|
|
726
1043
|
localServer.updateMainWindow(mainWindow);
|
|
727
1044
|
}
|
|
728
1045
|
|
|
1046
|
+
// El renderer que vuelve a cargar arranca creyendo que el overlay deja pasar el
|
|
1047
|
+
// mouse. Si habia quedado clickeable, nadie se lo iba a devolver: el renderer solo
|
|
1048
|
+
// avisa cuando CAMBIA de estado, y para el recien nacido no cambio nada. Se vuelve
|
|
1049
|
+
// al estado inicial a mano para que las dos mitades arranquen de acuerdo.
|
|
1050
|
+
pedirPassthrough(true);
|
|
1051
|
+
|
|
729
1052
|
loadRendererHtml(mainWindow, uuid);
|
|
730
1053
|
}
|
|
731
1054
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassnote/client",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.12",
|
|
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.
|
|
77
|
+
"@glassnote/renderer": "2.0.11",
|
|
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
|
-
|
|
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
|
+
};
|