figure-viewer 1.0.0

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/lib/history.js ADDED
@@ -0,0 +1,130 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const HISTORY_FILE = path.join(process.env.HOME || process.env.USERPROFILE, '.figure-viewer', 'history.json');
5
+
6
+ function ensureHistoryDir() {
7
+ const dir = path.dirname(HISTORY_FILE);
8
+ if (!fs.existsSync(dir)) {
9
+ fs.mkdirSync(dir, { recursive: true });
10
+ }
11
+ }
12
+
13
+ function loadHistory() {
14
+ ensureHistoryDir();
15
+ if (!fs.existsSync(HISTORY_FILE)) {
16
+ return {};
17
+ }
18
+ try {
19
+ return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ function saveHistory(history) {
26
+ ensureHistoryDir();
27
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
28
+ }
29
+
30
+ function getFigureStatus(figurePath, mtime) {
31
+ const now = Date.now();
32
+ const age = now - mtime;
33
+ const fiveMinutes = 5 * 60 * 1000;
34
+ const oneHour = 60 * 60 * 1000;
35
+
36
+ if (age < fiveMinutes) {
37
+ return 'active';
38
+ } else if (age < oneHour) {
39
+ return 'recent';
40
+ }
41
+ return 'older';
42
+ }
43
+
44
+ function getRelativeTime(mtime) {
45
+ const now = Date.now();
46
+ const diff = now - mtime;
47
+ const seconds = Math.floor(diff / 1000);
48
+ const minutes = Math.floor(seconds / 60);
49
+ const hours = Math.floor(minutes / 60);
50
+ const days = Math.floor(hours / 24);
51
+
52
+ if (seconds < 60) return 'just now';
53
+ if (minutes < 60) return `${minutes} min${minutes > 1 ? 's' : ''} ago`;
54
+ if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
55
+ return `${days} day${days > 1 ? 's' : ''} ago`;
56
+ }
57
+
58
+ function updateHistory(figuresDir, figures) {
59
+ const history = loadHistory();
60
+
61
+ if (!history[figuresDir]) {
62
+ history[figuresDir] = {};
63
+ }
64
+
65
+ const dirHistory = history[figuresDir];
66
+ const now = Date.now();
67
+
68
+ // Update or add figures
69
+ for (const figure of figures) {
70
+ const existing = dirHistory[figure.name];
71
+
72
+ if (existing) {
73
+ if (figure.mtime > existing.mtime) {
74
+ // Same file with newer mtime - increment version
75
+ dirHistory[figure.name] = {
76
+ mtime: figure.mtime,
77
+ version: existing.version + 1,
78
+ firstSeen: existing.firstSeen || existing.mtime
79
+ };
80
+ }
81
+ // If mtime hasn't changed, keep existing version
82
+ } else {
83
+ // New figure
84
+ dirHistory[figure.name] = {
85
+ mtime: figure.mtime,
86
+ version: 1,
87
+ firstSeen: figure.mtime
88
+ };
89
+ }
90
+ }
91
+
92
+ // Remove figures that no longer exist
93
+ const figureNames = new Set(figures.map(f => f.name));
94
+ for (const name of Object.keys(dirHistory)) {
95
+ if (!figureNames.has(name)) {
96
+ delete dirHistory[name];
97
+ }
98
+ }
99
+
100
+ saveHistory(history);
101
+ return dirHistory;
102
+ }
103
+
104
+ function getVersion(figurePath, history) {
105
+ const name = path.basename(figurePath);
106
+ return history[name]?.version || 1;
107
+ }
108
+
109
+ function clearHistory() {
110
+ if (fs.existsSync(HISTORY_FILE)) {
111
+ fs.unlinkSync(HISTORY_FILE);
112
+ }
113
+ }
114
+
115
+ function getHistoryForDir(figuresDir) {
116
+ const history = loadHistory();
117
+ return history[figuresDir] || {};
118
+ }
119
+
120
+ module.exports = {
121
+ loadHistory,
122
+ saveHistory,
123
+ getFigureStatus,
124
+ getRelativeTime,
125
+ updateHistory,
126
+ getVersion,
127
+ clearHistory,
128
+ getHistoryForDir,
129
+ HISTORY_FILE
130
+ };
package/lib/html.js ADDED
@@ -0,0 +1,486 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getFigureStatus, getRelativeTime, getVersion } = require('./history');
4
+ const { getConfig, DEFAULT_CONFIG } = require('./config');
5
+
6
+ const SUPPORTED_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.pdf', '.svg', '.gif', '.webp'];
7
+
8
+ function escapeHtml(str) {
9
+ return str
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&#039;');
15
+ }
16
+
17
+ function escapeFileUrl(filepath) {
18
+ // Use encodeURIComponent only for special chars, not the entire URL
19
+ const encoded = filepath.split('/').map(p => encodeURIComponent(p)).join('/');
20
+ return 'file://' + encoded;
21
+ }
22
+
23
+ function formatFileSize(bytes) {
24
+ if (bytes < 1024) return bytes + ' B';
25
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
26
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
27
+ }
28
+
29
+ function getImages(figuresDir) {
30
+ if (!fs.existsSync(figuresDir)) {
31
+ return [];
32
+ }
33
+
34
+ const files = fs.readdirSync(figuresDir);
35
+ const images = [];
36
+
37
+ for (const file of files) {
38
+ const ext = path.extname(file).toLowerCase();
39
+ if (!SUPPORTED_EXTENSIONS.includes(ext)) continue;
40
+
41
+ const filePath = path.join(figuresDir, file);
42
+ const stats = fs.statSync(filePath);
43
+
44
+ if (!stats.isFile()) continue;
45
+
46
+ images.push({
47
+ name: file,
48
+ path: filePath,
49
+ mtime: stats.mtimeMs,
50
+ size: stats.size,
51
+ ext: ext
52
+ });
53
+ }
54
+
55
+ // Sort by mtime, newest first
56
+ images.sort((a, b) => b.mtime - a.mtime);
57
+
58
+ return images;
59
+ }
60
+
61
+ function generateFigureCards(figuresDir, history) {
62
+ const images = getImages(figuresDir);
63
+
64
+ if (images.length === 0) {
65
+ return '';
66
+ }
67
+
68
+ return images.map((img, index) => {
69
+ const status = getFigureStatus(img.path, img.mtime);
70
+ const relativeTime = getRelativeTime(img.mtime);
71
+ const version = getVersion(img.path, history);
72
+
73
+ const statusColors = {
74
+ active: '#ef4444', // Red
75
+ recent: '#eab308', // Yellow
76
+ older: '#9ca3af' // Gray
77
+ };
78
+
79
+ const statusLabels = {
80
+ active: 'Active',
81
+ recent: 'Recent',
82
+ older: 'Older'
83
+ };
84
+
85
+ const borderColor = statusColors[status];
86
+ const fileUrl = escapeFileUrl(img.path);
87
+ const truncatedName = img.name.length > 25
88
+ ? img.name.substring(0, 22) + '...'
89
+ : img.name;
90
+
91
+ return `
92
+ <div class="figure-card"
93
+ data-index="${index}"
94
+ data-path="${escapeHtml(img.path)}"
95
+ data-name="${escapeHtml(img.name)}"
96
+ data-size="${img.size}"
97
+ data-mtime="${img.mtime}"
98
+ onclick="openLightbox(${index})">
99
+ <div class="figure-image" style="border-color: ${borderColor}">
100
+ <img src="${fileUrl}" alt="${escapeHtml(img.name)}" loading="lazy">
101
+ <span class="status-indicator" style="background-color: ${borderColor}"></span>
102
+ </div>
103
+ <div class="figure-info">
104
+ <div class="figure-name" title="${escapeHtml(img.name)}">${escapeHtml(truncatedName)}</div>
105
+ <div class="figure-meta">
106
+ <span class="figure-time">${relativeTime}</span>
107
+ ${version > 1 ? `<span class="figure-version">v${version}</span>` : ''}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ `.trim();
112
+ }).join('\n');
113
+ }
114
+
115
+ function generateHtml(figuresDir, options = {}) {
116
+ const images = getImages(figuresDir);
117
+ const history = options.history || {};
118
+ const config = options.config || getConfig();
119
+ const refreshInterval = config.refreshInterval || DEFAULT_CONFIG.refreshInterval;
120
+ const figureCards = generateFigureCards(figuresDir, history);
121
+
122
+ const hasImages = images.length > 0;
123
+ const emptyMessage = !hasImages
124
+ ? '<div class="empty-state"><p>No figures yet.</p><p>Run your analysis to generate figures.</p></div>'
125
+ : '';
126
+
127
+ const imagesJson = JSON.stringify(images.map(img => ({
128
+ name: img.name,
129
+ path: escapeFileUrl(img.path),
130
+ size: img.size,
131
+ mtime: img.mtime,
132
+ ext: img.ext
133
+ })));
134
+
135
+ return `<!DOCTYPE html>
136
+ <html lang="en">
137
+ <head>
138
+ <meta charset="UTF-8">
139
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
140
+ <title>Figure Viewer</title>
141
+ <style>
142
+ * {
143
+ margin: 0;
144
+ padding: 0;
145
+ box-sizing: border-box;
146
+ }
147
+ body {
148
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
149
+ background-color: #1a1a1a;
150
+ color: #e5e5e5;
151
+ min-height: 100vh;
152
+ }
153
+ .header {
154
+ display: flex;
155
+ justify-content: space-between;
156
+ align-items: center;
157
+ padding: 16px 24px;
158
+ background-color: #262626;
159
+ border-bottom: 1px solid #404040;
160
+ }
161
+ .header h1 {
162
+ font-size: 18px;
163
+ font-weight: 600;
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ }
168
+ .header-controls {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 16px;
172
+ }
173
+ .filter-input {
174
+ padding: 8px 12px;
175
+ border-radius: 6px;
176
+ border: 1px solid #404040;
177
+ background-color: #1a1a1a;
178
+ color: #e5e5e5;
179
+ font-size: 14px;
180
+ width: 200px;
181
+ }
182
+ .filter-input:focus {
183
+ outline: none;
184
+ border-color: #6366f1;
185
+ }
186
+ .refresh-btn {
187
+ padding: 8px 16px;
188
+ border-radius: 6px;
189
+ border: none;
190
+ background-color: #6366f1;
191
+ color: white;
192
+ font-size: 14px;
193
+ cursor: pointer;
194
+ transition: background-color 0.2s;
195
+ }
196
+ .refresh-btn:hover {
197
+ background-color: #4f46e5;
198
+ }
199
+ .stats {
200
+ padding: 12px 24px;
201
+ background-color: #262626;
202
+ border-bottom: 1px solid #404040;
203
+ font-size: 13px;
204
+ color: #9ca3af;
205
+ }
206
+ .grid {
207
+ display: grid;
208
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
209
+ gap: 20px;
210
+ padding: 24px;
211
+ }
212
+ .figure-card {
213
+ background-color: #262626;
214
+ border-radius: 12px;
215
+ overflow: hidden;
216
+ cursor: pointer;
217
+ transition: transform 0.2s, box-shadow 0.2s;
218
+ }
219
+ .figure-card:hover {
220
+ transform: translateY(-4px);
221
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
222
+ }
223
+ .figure-image {
224
+ position: relative;
225
+ aspect-ratio: 16/10;
226
+ background-color: #1a1a1a;
227
+ border-bottom: 3px solid;
228
+ overflow: hidden;
229
+ }
230
+ .figure-image img {
231
+ width: 100%;
232
+ height: 100%;
233
+ object-fit: contain;
234
+ }
235
+ .status-indicator {
236
+ position: absolute;
237
+ top: 8px;
238
+ right: 8px;
239
+ width: 10px;
240
+ height: 10px;
241
+ border-radius: 50%;
242
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
243
+ }
244
+ .figure-info {
245
+ padding: 12px;
246
+ }
247
+ .figure-name {
248
+ font-size: 14px;
249
+ font-weight: 500;
250
+ white-space: nowrap;
251
+ overflow: hidden;
252
+ text-overflow: ellipsis;
253
+ margin-bottom: 4px;
254
+ }
255
+ .figure-meta {
256
+ display: flex;
257
+ justify-content: space-between;
258
+ align-items: center;
259
+ font-size: 12px;
260
+ color: #9ca3af;
261
+ }
262
+ .figure-version {
263
+ background-color: #6366f1;
264
+ color: white;
265
+ padding: 2px 6px;
266
+ border-radius: 4px;
267
+ font-size: 11px;
268
+ font-weight: 600;
269
+ }
270
+ .empty-state {
271
+ display: flex;
272
+ flex-direction: column;
273
+ align-items: center;
274
+ justify-content: center;
275
+ height: 50vh;
276
+ color: #9ca3af;
277
+ }
278
+ .empty-state p {
279
+ margin: 8px 0;
280
+ }
281
+ .legend {
282
+ display: flex;
283
+ gap: 24px;
284
+ padding: 8px 0;
285
+ }
286
+ .legend-item {
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 6px;
290
+ font-size: 12px;
291
+ }
292
+ .legend-dot {
293
+ width: 10px;
294
+ height: 10px;
295
+ border-radius: 50%;
296
+ }
297
+ .legend-dot.active { background-color: #ef4444; }
298
+ .legend-dot.recent { background-color: #eab308; }
299
+ .legend-dot.older { background-color: #9ca3af; }
300
+
301
+ /* Lightbox */
302
+ .lightbox {
303
+ display: none;
304
+ position: fixed;
305
+ top: 0;
306
+ left: 0;
307
+ width: 100%;
308
+ height: 100%;
309
+ background-color: rgba(0, 0, 0, 0.95);
310
+ z-index: 1000;
311
+ flex-direction: column;
312
+ align-items: center;
313
+ justify-content: center;
314
+ }
315
+ .lightbox.active {
316
+ display: flex;
317
+ }
318
+ .lightbox-content {
319
+ max-width: 90vw;
320
+ max-height: 80vh;
321
+ position: relative;
322
+ }
323
+ .lightbox-content img {
324
+ max-width: 100%;
325
+ max-height: 80vh;
326
+ object-fit: contain;
327
+ }
328
+ .lightbox-close {
329
+ position: absolute;
330
+ top: -40px;
331
+ right: 0;
332
+ background: none;
333
+ border: none;
334
+ color: white;
335
+ font-size: 32px;
336
+ cursor: pointer;
337
+ padding: 8px;
338
+ }
339
+ .lightbox-nav {
340
+ position: absolute;
341
+ top: 50%;
342
+ transform: translateY(-50%);
343
+ background: rgba(255, 255, 255, 0.1);
344
+ border: none;
345
+ color: white;
346
+ font-size: 32px;
347
+ padding: 16px;
348
+ cursor: pointer;
349
+ border-radius: 8px;
350
+ transition: background 0.2s;
351
+ }
352
+ .lightbox-nav:hover {
353
+ background: rgba(255, 255, 255, 0.2);
354
+ }
355
+ .lightbox-prev { left: -60px; }
356
+ .lightbox-next { right: -60px; }
357
+ .lightbox-meta {
358
+ margin-top: 20px;
359
+ text-align: center;
360
+ color: #9ca3af;
361
+ }
362
+ .lightbox-meta .filename {
363
+ font-size: 18px;
364
+ font-weight: 600;
365
+ color: #e5e5e5;
366
+ margin-bottom: 8px;
367
+ }
368
+ .lightbox-meta .details {
369
+ font-size: 14px;
370
+ }
371
+ </style>
372
+ </head>
373
+ <body>
374
+ <div class="header">
375
+ <h1>Figure Viewer</h1>
376
+ <div class="header-controls">
377
+ <input type="text" class="filter-input" placeholder="Filter figures..." id="filterInput">
378
+ <button class="refresh-btn" onclick="location.reload()">Refresh</button>
379
+ </div>
380
+ </div>
381
+ <div class="stats">
382
+ <div class="legend">
383
+ <div class="legend-item"><span class="legend-dot active"></span> Active (&lt;5 min)</div>
384
+ <div class="legend-item"><span class="legend-dot recent"></span> Recent (5 min - 1 hour)</div>
385
+ <div class="legend-item"><span class="legend-dot older"></span> Older (&gt;1 hour)</div>
386
+ </div>
387
+ </div>
388
+ <div class="grid" id="figureGrid">
389
+ ${figureCards}
390
+ ${emptyMessage}
391
+ </div>
392
+
393
+ <div class="lightbox" id="lightbox">
394
+ <div class="lightbox-content">
395
+ <button class="lightbox-close" onclick="closeLightbox()">&times;</button>
396
+ <button class="lightbox-nav lightbox-prev" onclick="navigate(-1)">&#8249;</button>
397
+ <img id="lightboxImage" src="" alt="">
398
+ <button class="lightbox-nav lightbox-next" onclick="navigate(1)">&#8250;</button>
399
+ </div>
400
+ <div class="lightbox-meta">
401
+ <div class="filename" id="lightboxFilename"></div>
402
+ <div class="details" id="lightboxDetails"></div>
403
+ </div>
404
+ </div>
405
+
406
+ <script>
407
+ const images = ${imagesJson};
408
+ const refreshInterval = ${refreshInterval};
409
+ let currentIndex = 0;
410
+
411
+ // Filter functionality
412
+ const filterInput = document.getElementById('filterInput');
413
+ const figureCards = document.querySelectorAll('.figure-card');
414
+
415
+ filterInput.addEventListener('input', (e) => {
416
+ const query = e.target.value.toLowerCase();
417
+ figureCards.forEach(card => {
418
+ const name = card.dataset.name.toLowerCase();
419
+ card.style.display = name.includes(query) ? 'block' : 'none';
420
+ });
421
+ });
422
+
423
+ // Lightbox functionality
424
+ function openLightbox(index) {
425
+ currentIndex = index;
426
+ showImage();
427
+ document.getElementById('lightbox').classList.add('active');
428
+ document.body.style.overflow = 'hidden';
429
+ }
430
+
431
+ function closeLightbox() {
432
+ document.getElementById('lightbox').classList.remove('active');
433
+ document.body.style.overflow = '';
434
+ }
435
+
436
+ function navigate(direction) {
437
+ currentIndex += direction;
438
+ if (currentIndex < 0) currentIndex = images.length - 1;
439
+ if (currentIndex >= images.length) currentIndex = 0;
440
+ showImage();
441
+ }
442
+
443
+ function showImage() {
444
+ const img = images[currentIndex];
445
+ document.getElementById('lightboxImage').src = img.path;
446
+ document.getElementById('lightboxFilename').textContent = img.name;
447
+
448
+ const size = ${images.length} > 0 ? formatFileSize(images[currentIndex].size) : '';
449
+ document.getElementById('lightboxDetails').textContent = size;
450
+ }
451
+
452
+ function formatFileSize(bytes) {
453
+ if (bytes < 1024) return bytes + ' B';
454
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
455
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
456
+ }
457
+
458
+ // Keyboard navigation
459
+ document.addEventListener('keydown', (e) => {
460
+ const lightbox = document.getElementById('lightbox');
461
+ if (!lightbox.classList.contains('active')) return;
462
+
463
+ if (e.key === 'Escape') closeLightbox();
464
+ if (e.key === 'ArrowLeft') navigate(-1);
465
+ if (e.key === 'ArrowRight') navigate(1);
466
+ });
467
+
468
+ // Click outside to close
469
+ document.getElementById('lightbox').addEventListener('click', (e) => {
470
+ if (e.target.id === 'lightbox') closeLightbox();
471
+ });
472
+
473
+ // Auto-refresh every 30 seconds
474
+ setInterval(() => {
475
+ location.reload();
476
+ }, ${refreshInterval});
477
+ </script>
478
+ </body>
479
+ </html>`;
480
+ }
481
+
482
+ module.exports = {
483
+ generateHtml,
484
+ getImages,
485
+ SUPPORTED_EXTENSIONS
486
+ };
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ const chokidar = require('chokidar');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const projectRoot = process.argv[2]; // Now this is the output directory
8
+ const figuresDir = process.argv[3];
9
+ const outputPath = process.argv[4];
10
+ const refreshInterval = parseInt(process.argv[5]) || 30000;
11
+ const watcherTimeout = parseInt(process.argv[6]) || 3600000;
12
+ const pidFile = path.join(projectRoot, '.figure-viewer.pid');
13
+
14
+ let lastActivity = Date.now();
15
+
16
+ function generateAndSave() {
17
+ // Require fresh each time to get updated modules
18
+ delete require.cache[require.resolve(path.join(__dirname, '..', 'lib/html'))];
19
+ delete require.cache[require.resolve(path.join(__dirname, '..', 'lib/history'))];
20
+ delete require.cache[require.resolve(path.join(__dirname, '..', 'lib/config'))];
21
+
22
+ const { generateHtml, getImages } = require(path.join(__dirname, '..', 'lib/html'));
23
+ const { updateHistory } = require(path.join(__dirname, '..', 'lib/history'));
24
+ const { getConfig } = require(path.join(__dirname, '..', 'lib/config'));
25
+
26
+ const config = getConfig();
27
+ config.refreshInterval = refreshInterval;
28
+
29
+ const images = getImages(figuresDir);
30
+ const history = updateHistory(figuresDir, images);
31
+ const html = generateHtml(figuresDir, { history, config });
32
+ fs.writeFileSync(outputPath, html);
33
+ console.log('Regenerated at', new Date().toLocaleTimeString(), `(${images.length} figures)`);
34
+
35
+ lastActivity = Date.now();
36
+ }
37
+
38
+ // Save PID and metadata
39
+ fs.writeFileSync(pidFile, JSON.stringify({
40
+ pid: process.pid,
41
+ startTime: Date.now(),
42
+ figuresDir,
43
+ outputPath
44
+ }));
45
+
46
+ // Cleanup on exit
47
+ process.on('exit', () => {
48
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
49
+ });
50
+
51
+ process.on('SIGINT', () => {
52
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
53
+ process.exit(0);
54
+ });
55
+
56
+ process.on('SIGTERM', () => {
57
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
58
+ process.exit(0);
59
+ });
60
+
61
+ // Check for inactivity timeout
62
+ const inactivityCheck = setInterval(() => {
63
+ const idleTime = Date.now() - lastActivity;
64
+ if (idleTime > watcherTimeout) {
65
+ console.log(`No activity for ${Math.round(idleTime/60000)} minutes. Stopping watcher...`);
66
+ clearInterval(inactivityCheck);
67
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
68
+ process.exit(0);
69
+ }
70
+ }, 60000); // Check every minute
71
+
72
+ const watcher = chokidar.watch(figuresDir, {
73
+ ignored: /(^|[\/\\])\../,
74
+ persistent: true,
75
+ ignoreInitial: true,
76
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
77
+ });
78
+
79
+ watcher.on('add', (file) => {
80
+ console.log('New figure:', path.basename(file));
81
+ generateAndSave();
82
+ });
83
+ watcher.on('change', (file) => {
84
+ console.log('Changed:', path.basename(file));
85
+ generateAndSave();
86
+ });
87
+ watcher.on('unlink', (file) => {
88
+ console.log('Removed:', path.basename(file));
89
+ generateAndSave();
90
+ });
91
+ watcher.on('error', (error) => {
92
+ console.error('Watcher error:', error.message);
93
+ });
94
+
95
+ console.log('Watching', figuresDir, 'for changes... (PID:', process.pid + ')');
96
+ console.log('Will stop after', Math.round(watcherTimeout/60000), 'minutes of inactivity');