Repository navigation
Expand file tree
/
Copy pathmacros.js
More file actions
818 lines (778 loc) · 29.3 KB
/
Copy pathmacros.js
File metadata and controls
818 lines (778 loc) · 29.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
/*
* macros.js
*
* Functions and data relating to macros.
*
* Macros are sort of "canned routines" that are analagous to the "custom cuts" in SB3.
* They are in fact, invoked in the same way in the OpenSBP runtime as they were in SB3,
* by using the C# command (C3 to home the tool, C2 for Z-zero, etc.)
*
* Macros are stored on disk at (for example) /opt/fabmo/macros - anything in this directory is scanned
* at startup and files containing an appropriate header are loaded into memory. Ideally macros can be
* in any file format, but the OpenSBP format is the only one that is actually implemented right now.
* When macros are modified by the user they are saved back to the files that they were loaded from. The
* header in each macro file contains metadata that identifies the macro, its custom-cut number, and description
*
* The macro headers are part of the files, but they are not displayed to the user when editing. The user
* is able to edit those fields, but only as exposed through the UI in the macro manager. This prevents
* users from corrupting the headers and creating a bunch of edge cases when editing macros.
*/
var fs = require("fs-extra");
var path = require("path");
var async = require("async");
var crypto = require("crypto");
var config = require("./config");
var log = require("./log").logger("macro");
// The marker in the header that signifies a macro.
// TODO - This is used to create files, but not in the regexs used to parse them (see below)
var MARKER = "!FABMO!";
// All the loaded macros will be stored here
var macros = {};
// These functions create macro headers from the specified options
// options:
// name - The macro display name
// description - The macro description
// enabled - Whether or not the macro is enabled (TODO: Is this used?)
var _createGCodeHeader = function (options) {
var name = options.name || "Untitled Macro";
var description = options.description || "";
var enabled = options.enabled || true;
return (
"(" +
MARKER +
"name:" +
name +
")\n" +
"(" +
MARKER +
"description:" +
description +
")\n" +
"(" +
MARKER +
"enabled:" +
enabled +
")\n"
);
};
var _createOpenSBPHeader = function (options) {
var name = options.name || "Untitled Macro";
var description = options.description || "";
var enabled = options.enabled || true;
return (
"'" +
MARKER +
"name:" +
name +
"\n" +
"'" +
MARKER +
"description:" +
description +
"\n" +
"'" +
MARKER +
"enabled:" +
enabled +
"\n"
);
};
var _deleteMacroFile = function (index, callback) {
var macro_path = config.getDataDir("macros");
var opensbp = path.join(macro_path, "macro_" + index + ".sbp");
var gcode = path.join(macro_path, "macro_" + index + ".nc");
// eslint-disable-next-line no-unused-vars
fs.unlink(opensbp, function (err) {
// eslint-disable-next-line no-unused-vars
fs.unlink(gcode, function (err) {
callback(null);
});
});
};
// Given a number and a type, construct a path to the corresponding macro file
var _createMacroFilename = function (id, type) {
var macro_path = config.getDataDir("macros");
switch (type) {
case "nc":
return path.join(macro_path, "macro_" + id + ".nc");
case "sbp":
return path.join(macro_path, "macro_" + id + ".sbp");
default:
throw new Error("Invalid macro type: " + type);
}
};
// Create default macro content for the specified macro.
// (Use if you want "new" macros to be non-empty)
var _createMacroDefaultContent = function (macro) {
switch (macro.type) {
case "nc":
//return '( ' + macro.name + ' )\n( ' + macro.description + ' )\n\n';
return "";
case "sbp":
//return "' " + macro.name + "\n' " + macro.description + "\n\n";
return "";
default:
throw new Error("Invalid macro type: " + macro.type);
}
};
// Iterate over the lines in the macro file, and parse out lines that appear to be part of the header
// filename - The filename of the macro to parse out
// callback - called with the parsed contents of the macro file, eg:
// {name : 'My Macro', description:'Move to X=10',content : 'MZ,0.5\nMX,10'}
var _parseMacroFile = function (filename, callback) {
var re = /[(']!FABMO!(\w+):([^)]*)\)?/;
var obj = {};
var ok = false;
fs.readFile(filename, function (err, data) {
if (err) {
log.error(err);
} else {
var lines = data.toString().split("\n");
var i = 0;
while (i < lines.length) {
var line = lines[i];
var groups = line.match(re);
if (groups) {
ok = true;
var key = groups[1];
var value = groups[2];
obj[key] = value;
} else {
break;
}
i += 1;
}
if (ok) {
obj.filename = filename;
obj.content = lines.slice(i, lines.length).join("\n");
callback(null, obj);
} else {
try {
log.error("File " + filename + " failed to parse. Unlinking it so it can be replaced.");
fs.unlink(filename);
} finally {
callback(null, undefined);
}
}
}
});
};
// Update an existing macro with new content
// id - The macro to update
// macro - The macro object that contains the new content
// callback - called on completion, with an error if appropriate
var update = function (id, macro, callback) {
// Get the old macro data
var old_macro = get(id);
if (old_macro) {
// Here, we're updating an existing macro
// We only update fields that were provided in the macro passed in
// Other fields, we leave alone.
// Tried moving this function to outer scope but it caused issues with
// saving macros, will address later
// eslint-disable-next-line no-inner-declarations
function savemacro(id, callback) {
old_macro.name = macro.name || old_macro.name;
old_macro.description = macro.description || old_macro.description;
old_macro.content = macro.content || old_macro.content;
old_macro.index = macro.index || old_macro.index;
old_macro.type = macro.type || old_macro.type;
old_macro.filename = _createMacroFilename(old_macro.index, old_macro.type);
save(id, callback);
}
// This function takes an id, and the macro can carry an index as well
// If the incoming macros index is different than the id that was passed,
// we interpret that as an intent to move that macro to a new index.
if (macro.index) {
var new_index = parseInt(macro.index);
// If there's already a macro at the index that we're moving to, that's an error.
// we're not going to write it.
if (get(new_index)) {
return callback(new Error("There is already a macro #" + new_index));
}
// If the new index is different we actually want to move the macro,
// so we assign it to the new index, trash the macro at the old index
// trash the file at the old index, and finally save the file at the new index
if (new_index != old_macro.index) {
macros[new_index] = old_macro;
delete macros[old_macro.index];
// eslint-disable-next-line no-unused-vars
_deleteMacroFile(old_macro.index, function (err) {
savemacro(new_index, callback);
});
} else {
// Provided an index with the macro, but it's the the same, so no move needed
savemacro(id, callback);
}
} else {
// Not moving the macro (didn't provide an index) so just save it
savemacro(id, callback);
}
} else {
// In this case, we're "updating" a macro that doesn't exist, so create a new one
// (filling in any attributes that were not provided by the update)
var new_macro = {
name: macro.name || "Untitled Macro",
description: macro.description || "Macro Description",
type: macro.type || "sbp",
enabled: macro.enabled || true, // TODO fix this
index: id,
};
new_macro.filename = _createMacroFilename(id, new_macro.type);
new_macro.content = macro.content || _createMacroDefaultContent(new_macro);
macros[id] = new_macro;
save(id, callback);
}
};
// Commit the provided macro id to disk.
// callback is called with the macro object that was saved (or error)
var save = function (id, callback) {
var macro = get(id);
if (macro) {
var macro_path = config.getDataDir("macros");
var file_path = path.join(macro_path, "macro_" + macro.index + "." + macro.type);
switch (macro.type) {
case "nc":
var header = _createGCodeHeader(macro);
break;
case "sbp":
// eslint-disable-next-line no-redeclare
var header = _createOpenSBPHeader(macro);
break;
default:
setImmediate(callback, new Error("Invalid macro type: " + macro.type));
break;
}
fs.open(file_path, "w", function (err, fd) {
if (err) {
log.error(err);
return callback(err);
}
let contentString = header + macro.content;
var contents = Buffer.from(contentString);
fs.write(
fd,
contents,
0,
contents.length,
0,
// eslint-disable-next-line no-unused-vars
function (err, written, string) {
if (err) {
log.error(err);
return callback(err);
}
fs.fsync(fd, function (err) {
if (err) {
log.error(err);
}
fs.closeSync(fd);
log.debug("fsync()ed " + file_path);
callback(err, macro);
});
},
);
});
} else {
callback(new Error("No such macro " + id));
}
};
// Load all macros from disk
var load = function (callback) {
var macro_path = config.getDataDir("macros");
var re = /macro_([0-9]+)\.(nc|sbp)/;
macros = {};
fs.readdir(macro_path, function (err, files) {
if (err) {
callback(err);
} else {
for (var i = 0; i < files.length; i++) {
files[i] = path.join(macro_path, files[i]);
}
async.map(files, _parseMacroFile, function (err, results) {
results.forEach(function (info) {
if (info) {
var groups = info.filename.match(re);
if (groups) {
var idx = parseInt(groups[1]);
var ext = groups[2];
info.index = idx;
info.type = ext;
macros[idx] = info;
}
}
});
callback(null);
});
}
});
};
// Return the full list of macros (scrubbed)
var list = function () {
var retval = [];
for (var key in macros) {
retval.push(getInfo(key));
}
return retval;
};
// Get the metadata for a macro by index (null if no macro with that index)
var getInfo = function (idx) {
var macro = get(idx);
if (macro) {
return {
name: macro.name,
description: macro.description,
enabled: macro.enabled,
type: macro.type,
index: parseInt(macro.index),
};
} else {
return null;
}
};
// Retrieve a macro by index
var get = function (idx) {
return macros[idx] || null;
};
// Run a macro by index.
var run = function (idx) {
var machine = require("./machine").machine;
var bypassInterlock = false;
var info = macros[idx];
log.debug(idx);
log.debug(info);
if (parseInt(idx) === 2) {
bypassInterlock = true;
}
if (info) {
machine.runFile(info.filename, bypassInterlock);
} else {
throw new Error("No such macro.");
}
};
// Delete a macro by index
// callback returns an error only
var del = function (idx, callback) {
var info = macros[idx];
if (info) {
_deleteMacroFile(idx, function (err) {
if (err) {
callback(err);
} else {
delete macros[idx];
callback(null);
}
});
} else {
callback(new Error("No such macro: " + idx));
}
};
// Assemble the installed macro set at <data>/macros from the shipped
// sources (see _getMacroSourceDirs): the common set in the default
// profile, the feature subdirectories enabled by machine.features, and
// any overrides the current profile ships. Copy-if-not-exists, so
// user-edited macros always win and survive restarts and updates.
// Feature subdirectories are flattened -- the installed directory stays
// flat because load() does not recurse. Disabling a feature does not
// remove its macros (they may carry user edits; restores everywhere in
// FabMo are additive).
var installProfileMacros = function (callback) {
var installedMacrosDir = config.getDataDir("macros");
var re = /^macro_([0-9]+)\.(nc|sbp)$/;
var claimed = {}; // basename -> true; first (highest-precedence) source wins
var jobs = [];
_getMacroSourceDirs().forEach(function (dir) {
var files;
try {
files = fs.readdirSync(dir);
} catch (e) {
return;
}
files.forEach(function (fn) {
var m = re.exec(fn);
if (!m || claimed[fn]) {
return;
}
claimed[fn] = true;
jobs.push({ src: path.join(dir, fn), dst: path.join(installedMacrosDir, fn), index: m[1] });
});
});
// Policy (2026-10, see doc/update-settings-merge.md): a missing macro is
// installed from the shipped default; an installed macro the user has
// NEVER edited (its hash matches the recorded base in macros_meta.json)
// is auto-updated when a newer default ships — a stock macro has no
// customization to protect, and critical fixes must not wait for the
// user to notice a badge. An edited macro is never touched here; the
// macro manager surfaces it as customized/diverged for a manual review.
var meta = _readMeta();
var metaDirty = false;
var installOrUpdate = function (job, callback) {
fs.stat(job.dst, function (err, stats) {
if (err || !stats.isFile()) {
log.debug("Installing macro " + job.src + " -> " + job.dst);
return fs.copy(job.src, job.dst, function (copyErr) {
if (!copyErr) {
meta.macros[job.index] = { base: _hashFile(job.src) };
metaDirty = true;
}
callback(copyErr);
});
}
var srcHash = _hashFile(job.src);
var dstHash = _hashFile(job.dst);
if (!srcHash || !dstHash || srcHash === dstHash) {
return callback();
}
var base = (meta.macros[job.index] || {}).base;
if (base === dstHash) {
log.info(
"Macro " + path.basename(job.dst) + " is unmodified and a newer default shipped - auto-updating"
);
return fs.copy(job.src, job.dst, function (copyErr) {
if (!copyErr) {
meta.macros[job.index] = { base: srcHash };
metaDirty = true;
}
callback(copyErr);
});
}
// Customized (or unknown history): leave it strictly alone.
callback();
});
};
async.eachSeries(jobs, installOrUpdate, function (err) {
if (metaDirty) {
_writeMeta(meta);
}
callback(err);
});
};
// ---- Shipped-default (profile) macro tracking -----------------------------
//
// Profile macros are copied into the data directory once (when the profile
// is applied) and never touched again, so user edits survive engine
// updates -- but newly shipped macro revisions never arrive either. The
// functions below compare the installed macros against the shipped
// defaults for the current profile and track, per macro, a hash of the
// default each installed copy was last synced from (its "base"). That
// lets the macro manager distinguish "you customized this" from "a newer
// default version arrived with an update" and offer a per-macro
// update / revert (both are installDefault -- the difference is only in
// how the UI words it).
//
// The base hashes live in <data>/config/macros_meta.json -- deliberately
// NOT in the macros directory, because load() unlinks any file there
// that does not parse as a macro.
function _metaPath() {
return path.join(config.getDataDir("config"), "macros_meta.json");
}
function _readMeta() {
try {
var meta = JSON.parse(fs.readFileSync(_metaPath()));
meta.macros = meta.macros || {};
return meta;
} catch (e) {
// Every engine update deletes /opt/fabmo — including this file —
// on exactly the boot where the base hashes decide whether stock
// macros may auto-update. Fall back to the backup mirror's copy
// (macros are restored from the same mirror by
// createDataDirectories, so the hashes match the restored files).
try {
var backup = JSON.parse(fs.readFileSync("/opt/fabmo_backup/config/" + path.basename(_metaPath())));
backup.macros = backup.macros || {};
log.info("macros_meta.json missing - recovered base hashes from backup mirror");
return backup;
} catch (e2) {
return { macros: {} };
}
}
}
function _writeMeta(meta) {
try {
fs.writeFileSync(_metaPath(), JSON.stringify(meta, null, 2));
} catch (e) {
log.warn("Could not write " + _metaPath() + ": " + e.message);
}
}
// Hash macro file content for comparison. Line endings and trailing
// whitespace are normalized so a copy that round-tripped through an
// editor or another OS does not read as a modification.
function _hashContent(data) {
var normalized = data.toString().replace(/\r\n/g, "\n").replace(/\s+$/, "");
return crypto.createHash("sha1").update(normalized).digest("hex");
}
function _hashFile(filename) {
try {
return _hashContent(fs.readFileSync(filename));
} catch (e) {
return null;
}
}
// Resolve the current profile's own macros directory (overrides shipped
// on top of the common set), or null if the profile does not ship one.
// The engine config may hold either the profile's directory name or its
// display name (from the profile's package.json), so match both, case-
// insensitively. Prefer the engine's own ./profiles copy, which is always
// current with the installed engine version -- the mirror under the data
// directory is only copied when missing, so it goes stale across updates.
// Custom (profile designer) profiles exist only under the data directory.
function _getProfileMacrosDir() {
var current = config.engine.get("profile") || "default";
var registry = require("./profiles").getProfiles();
var profileDir = null;
Object.keys(registry).forEach(function (name) {
var p = registry[name];
if (
name.toLowerCase() === current.toLowerCase() ||
path.basename(p.dir).toLowerCase() === current.toLowerCase()
) {
profileDir = p.dir;
}
});
if (!profileDir) {
profileDir = path.join(config.getDataDir("profiles"), current);
}
var repoDir = path.join(__dirname, "profiles", path.basename(profileDir), "macros");
if (fs.existsSync(repoDir)) {
return repoDir;
}
var dataDir = path.join(profileDir, "macros");
if (fs.existsSync(dataDir)) {
return dataDir;
}
return null;
}
// Ordered list of shipped-macro source directories for this machine,
// highest precedence first:
// 1. the current profile's own macros directory (overrides)
// 2. profiles/default/macros/<feature>/ for each enabled machine
// feature flag (machine.features -- atc, laser, knife, ...)
// 3. profiles/default/macros -- the common set shipped with every tool
function _getMacroSourceDirs() {
var dirs = [];
var commonDir = path.join(__dirname, "profiles", "default", "macros");
if (!fs.existsSync(commonDir)) {
// Fall back to the data-directory mirror of the default profile
commonDir = path.join(config.getDataDir("profiles"), "default", "macros");
}
var profileDir = _getProfileMacrosDir();
if (profileDir && profileDir !== commonDir) {
dirs.push(profileDir);
}
var features = (config.machine && config.machine.get("features")) || {};
Object.keys(features).forEach(function (feature) {
if (features[feature]) {
var featureDir = path.join(commonDir, feature);
if (fs.existsSync(featureDir)) {
dirs.push(featureDir);
}
}
});
if (fs.existsSync(commonDir)) {
dirs.push(commonDir);
}
return dirs;
}
// List the shipped default macro files for this machine, keyed by index:
// { 2 : {filename : '/fabmo/profiles/.../macro_2.sbp', type : 'sbp'}, ... }
// Sources are consulted in precedence order; the first one to supply an
// index wins, matching what installProfileMacros installs.
function _listDefaultMacros() {
var defaults = {};
var re = /^macro_([0-9]+)\.(nc|sbp)$/;
_getMacroSourceDirs().forEach(function (dir) {
try {
fs.readdirSync(dir).forEach(function (fn) {
var groups = fn.match(re);
if (groups && !(parseInt(groups[1]) in defaults)) {
defaults[parseInt(groups[1])] = {
filename: path.join(dir, fn),
type: groups[2],
};
}
});
} catch (e) {
log.warn("Could not read default macros from " + dir + ": " + e.message);
}
});
return defaults;
}
// Read a macro file synchronously into {name, description, ..., content}
// (header fields plus header-stripped content) without the
// unlink-on-parse-failure behavior of _parseMacroFile (we must never
// delete files out of a profile directory). Returns null if unreadable.
function _readMacroFileSync(filename) {
var re = /[(']!FABMO!(\w+):([^)]*)\)?/;
var obj = {};
try {
var lines = fs.readFileSync(filename).toString().split("\n");
var i = 0;
while (i < lines.length) {
var groups = lines[i].match(re);
if (!groups) {
break;
}
obj[groups[1]] = groups[2];
i++;
}
obj.content = lines.slice(i).join("\n");
} catch (e) {
return null;
}
return obj;
}
// Read just the header fields (name, description, ...)
function _readHeaderInfo(filename) {
return _readMacroFileSync(filename) || {};
}
// Get the shipped default for a macro index (null if the current profile
// does not ship one): {index, type, name, description, content}
var getDefault = function (idx) {
idx = parseInt(idx);
var def = _listDefaultMacros()[idx];
if (!def) {
return null;
}
var parsed = _readMacroFileSync(def.filename);
if (!parsed) {
return null;
}
return {
index: idx,
type: def.type,
name: parsed.name || "macro_" + idx,
description: parsed.description || "",
content: parsed.content || "",
};
};
// Compute the status of every macro (installed and/or shipped) vs the
// current profile's defaults. Returns a list sorted by index:
// { index, installed, has_default, state, name?, description? }
// states:
// custom - installed macro with no shipped counterpart
// current - installed copy matches the shipped default
// customized - user edited it; the default is unchanged since sync
// update_available - the default changed; the installed copy was never edited
// diverged - both differ (edits + a new default, or unknown history)
// new_default - shipped default exists but no macro is installed
// ignored_default - like new_default, but the user chose to dismiss it
// Base recording self-heals: whenever installed == default the base hash
// is (re)recorded, so machines that predate this feature converge without
// a migration step.
var getStatus = function () {
var defaults = _listDefaultMacros();
var meta = _readMeta();
var dirty = false;
var indices = {};
Object.keys(macros).forEach(function (k) {
indices[k] = true;
});
Object.keys(defaults).forEach(function (k) {
indices[k] = true;
});
var result = [];
Object.keys(indices)
.map(Number)
.sort(function (a, b) {
return a - b;
})
.forEach(function (idx) {
var installed = macros[idx];
var def = defaults[idx];
var entry = {
index: idx,
installed: !!installed,
has_default: !!def,
};
if (installed && !def) {
entry.state = "custom";
} else if (!installed && def) {
var header = _readHeaderInfo(def.filename);
entry.name = header.name || "macro_" + idx;
entry.description = header.description || "";
var seen = meta.macros[idx];
entry.state = seen && seen.base === _hashFile(def.filename) ? "ignored_default" : "new_default";
} else {
var defHash = _hashFile(def.filename);
var instHash = _hashFile(installed.filename);
var base = (meta.macros[idx] || {}).base;
if (defHash === instHash) {
entry.state = "current";
if (base !== defHash) {
meta.macros[idx] = { base: defHash };
dirty = true;
}
} else if (base === defHash) {
entry.state = "customized";
} else if (base === instHash) {
entry.state = "update_available";
} else {
entry.state = "diverged";
}
}
result.push(entry);
});
if (dirty) {
_writeMeta(meta);
}
return result;
};
// Replace the installed macro at idx with the shipped default (also used
// to install a shipped macro that is missing). Records the new base hash
// and reloads the macro into memory.
var installDefault = function (idx, callback) {
idx = parseInt(idx);
var def = _listDefaultMacros()[idx];
if (!def) {
return callback(new Error("No shipped default for macro " + idx));
}
// Remove any existing files at this index first -- the installed copy
// may be a different type (.nc vs .sbp) than the default.
_deleteMacroFile(idx, function () {
var dest = _createMacroFilename(idx, def.type);
fs.copy(def.filename, dest, function (err) {
if (err) {
return callback(err);
}
var meta = _readMeta();
meta.macros[idx] = { base: _hashFile(def.filename) };
_writeMeta(meta);
delete macros[idx];
_parseMacroFile(dest, function (err, info) {
if (info) {
info.index = idx;
info.type = def.type;
macros[idx] = info;
}
callback(err || null, getInfo(idx));
});
});
});
};
// Mark the current shipped default for idx as "seen" without installing
// it: records its hash as the base so the update badge goes away until
// the default changes again.
var dismissDefault = function (idx, callback) {
idx = parseInt(idx);
var def = _listDefaultMacros()[idx];
if (!def) {
return callback(new Error("No shipped default for macro " + idx));
}
var meta = _readMeta();
meta.macros[idx] = { base: _hashFile(def.filename) };
_writeMeta(meta);
callback(null);
};
exports.load = load;
exports.list = list;
exports.get = get;
exports.del = del;
exports.run = run;
exports.getInfo = getInfo;
exports.update = update;
exports.save = save;
exports.installProfile = installProfileMacros;
exports.getStatus = getStatus;
exports.installDefault = installDefault;
exports.dismissDefault = dismissDefault;
exports.getDefault = getDefault;