@underactive/pi-topping 0.3.1 → 0.5.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/README.md +59 -17
- package/package.json +5 -5
- package/src/activity-meter.ts +11 -8
- package/src/format.ts +159 -47
- package/src/menu.ts +84 -23
- package/src/preview.ts +115 -32
- package/src/prompt-decorator.ts +85 -10
- package/src/session.ts +184 -58
- package/src/settings.ts +168 -38
package/src/settings.ts
CHANGED
|
@@ -1,63 +1,134 @@
|
|
|
1
1
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { DEFAULT_LOADER_ORDER, type LoaderElement } from "./format.ts";
|
|
4
5
|
import type { MenuSection } from "./menu.ts";
|
|
5
6
|
|
|
7
|
+
export const SETTING_COLOR_VALUES = ["accent", "border", "borderAccent", "success", "error", "warning"] as const;
|
|
8
|
+
export type SettingColor = (typeof SETTING_COLOR_VALUES)[number];
|
|
9
|
+
|
|
10
|
+
export const BORDER_STYLE_VALUES = ["double", "single", "rounded", "heavy"] as const;
|
|
11
|
+
export type BorderStyle = (typeof BORDER_STYLE_VALUES)[number];
|
|
12
|
+
export const DONE_MARKER_BORDER_STYLE_VALUES = [...BORDER_STYLE_VALUES, "none"] as const;
|
|
13
|
+
export type DoneMarkerBorderStyle = (typeof DONE_MARKER_BORDER_STYLE_VALUES)[number];
|
|
14
|
+
|
|
15
|
+
export function isBorderStyle(value: unknown): value is BorderStyle {
|
|
16
|
+
return typeof value === "string" && BORDER_STYLE_VALUES.some(style => style === value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isDoneMarkerBorderStyle(value: unknown): value is DoneMarkerBorderStyle {
|
|
20
|
+
return typeof value === "string" && DONE_MARKER_BORDER_STYLE_VALUES.some(style => style === value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isSettingColor(value: unknown): value is SettingColor {
|
|
24
|
+
return typeof value === "string" && SETTING_COLOR_VALUES.some(color => color === value);
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
export interface DecoratorSettings {
|
|
7
28
|
decorations: {
|
|
8
29
|
animatedSpinner: boolean;
|
|
9
30
|
shimmer: boolean;
|
|
31
|
+
shimmerInverted: boolean;
|
|
10
32
|
shimmerDirection: "ltr" | "rtl";
|
|
11
33
|
shimmerDirectionEnabled: boolean;
|
|
34
|
+
shimmerSpeed: "slow" | "normal" | "fast";
|
|
35
|
+
shimmerSpeedEnabled: boolean;
|
|
12
36
|
tokenActivityMonitor: boolean;
|
|
13
37
|
meterDirection: "ltr" | "rtl";
|
|
14
38
|
meterDirectionEnabled: boolean;
|
|
15
39
|
decorateUserPrompt: boolean;
|
|
16
|
-
borderColor:
|
|
40
|
+
borderColor: SettingColor;
|
|
17
41
|
borderColorEnabled: boolean;
|
|
18
|
-
|
|
42
|
+
borderStyle: BorderStyle;
|
|
43
|
+
borderStyleEnabled: boolean;
|
|
44
|
+
doneMarkerBorderStyle: DoneMarkerBorderStyle;
|
|
45
|
+
doneMarkerBorderColor: SettingColor;
|
|
46
|
+
spinnerColor: SettingColor;
|
|
19
47
|
spinnerColorEnabled: boolean;
|
|
20
|
-
meterColor:
|
|
48
|
+
meterColor: SettingColor;
|
|
21
49
|
meterColorEnabled: boolean;
|
|
22
50
|
meterDimmed: boolean;
|
|
51
|
+
tokenRateColor: SettingColor;
|
|
52
|
+
tokenRateDimmed: boolean;
|
|
23
53
|
promptIcon: boolean;
|
|
24
54
|
promptTimestamp: boolean;
|
|
55
|
+
promptProvider: boolean;
|
|
56
|
+
promptModel: boolean;
|
|
25
57
|
useNerdFont: boolean;
|
|
26
58
|
};
|
|
27
59
|
features: {
|
|
28
60
|
substituteDefaultMessage: boolean;
|
|
29
61
|
elapsedTime: boolean;
|
|
30
62
|
outputTokens: boolean;
|
|
63
|
+
tokenRate: boolean;
|
|
31
64
|
doneMarker: boolean;
|
|
32
65
|
doneMarkerIcon: boolean;
|
|
33
66
|
randomizeDoneMarker: boolean;
|
|
34
67
|
doneMarkerTokens: boolean;
|
|
68
|
+
doneMarkerInputs: boolean;
|
|
35
69
|
};
|
|
70
|
+
loaderOrder: LoaderElement[];
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
export const DEFAULT_SETTINGS: DecoratorSettings = {
|
|
39
|
-
decorations: { animatedSpinner: true, shimmer: true, shimmerDirection: "ltr", shimmerDirectionEnabled: true, tokenActivityMonitor: true, meterDirection: "
|
|
40
|
-
features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true },
|
|
74
|
+
decorations: { animatedSpinner: true, shimmer: true, shimmerInverted: false, shimmerDirection: "ltr", shimmerDirectionEnabled: true, shimmerSpeed: "normal", shimmerSpeedEnabled: true, tokenActivityMonitor: true, meterDirection: "rtl", meterDirectionEnabled: true, decorateUserPrompt: true, borderColor: "borderAccent", borderColorEnabled: true, borderStyle: "double", borderStyleEnabled: true, doneMarkerBorderStyle: "none", doneMarkerBorderColor: "borderAccent", spinnerColor: "accent", spinnerColorEnabled: true, meterColor: "accent", meterColorEnabled: true, meterDimmed: false, tokenRateColor: "warning", tokenRateDimmed: false, promptIcon: true, promptTimestamp: true, promptProvider: true, promptModel: true, useNerdFont: true },
|
|
75
|
+
features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, tokenRate: true, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true, doneMarkerInputs: true },
|
|
76
|
+
loaderOrder: [...DEFAULT_LOADER_ORDER],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Menu key carrying the loader element order as a comma-joined list of element ids. */
|
|
80
|
+
export const LOADER_ORDER_ID = "loaderOrder";
|
|
81
|
+
const LOADER_ELEMENT_LABELS: Record<LoaderElement, string> = {
|
|
82
|
+
spinner: "Animated spinner",
|
|
83
|
+
text: "“Working” text",
|
|
84
|
+
meter: "Token activity monitor",
|
|
85
|
+
elapsed: "Elapsed time",
|
|
86
|
+
tokens: "Output tokens",
|
|
87
|
+
tokenRate: "Token rate",
|
|
41
88
|
};
|
|
42
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Normalize an element order from disk or from the menu, tolerating hand edits and
|
|
92
|
+
* version skew: unknown entries and duplicates are dropped, missing elements are
|
|
93
|
+
* appended in their default positions.
|
|
94
|
+
*/
|
|
95
|
+
export function parseLoaderOrder(value: unknown): LoaderElement[] {
|
|
96
|
+
const entries = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : [];
|
|
97
|
+
const order: LoaderElement[] = [];
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const element = typeof entry === "string" ? entry.trim() as LoaderElement : undefined;
|
|
100
|
+
if (element && DEFAULT_LOADER_ORDER.includes(element) && !order.includes(element)) order.push(element);
|
|
101
|
+
}
|
|
102
|
+
for (const element of DEFAULT_LOADER_ORDER) if (!order.includes(element)) order.push(element);
|
|
103
|
+
return order;
|
|
104
|
+
}
|
|
105
|
+
|
|
43
106
|
export function settingsPath(): string { return join(getAgentDir(), "pi-topping", "settings.json"); }
|
|
44
107
|
function isPlainObject(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
45
108
|
function mergeGroup<T extends Record<string, boolean | string>>(defaults: T, parsed: unknown): T {
|
|
46
109
|
const merged = { ...defaults };
|
|
47
110
|
if (!isPlainObject(parsed)) return merged;
|
|
48
111
|
for (const [key, value] of Object.entries(parsed)) {
|
|
49
|
-
if (!(key
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
if (key
|
|
112
|
+
if (!Object.hasOwn(merged, key)) continue;
|
|
113
|
+
let valid: boolean | string | undefined;
|
|
114
|
+
if (typeof merged[key] === "boolean" && typeof value === "boolean") valid = value;
|
|
115
|
+
else if ((key === "meterDirection" || key === "shimmerDirection") && (value === "ltr" || value === "rtl")) valid = value;
|
|
116
|
+
else if (key === "shimmerSpeed" && (value === "slow" || value === "normal" || value === "fast")) valid = value;
|
|
117
|
+
else if (key.endsWith("Color") && isSettingColor(value)) valid = value;
|
|
118
|
+
else if (key === "borderStyle" && isBorderStyle(value)) valid = value;
|
|
119
|
+
else if (key === "doneMarkerBorderStyle" && isDoneMarkerBorderStyle(value)) valid = value;
|
|
120
|
+
if (valid !== undefined) (merged as Record<string, boolean | string>)[key] = valid;
|
|
53
121
|
}
|
|
54
122
|
return merged;
|
|
55
123
|
}
|
|
56
124
|
|
|
57
|
-
/** Map a persisted setting value to its menu cycle label (directions only; colors pass through). */
|
|
125
|
+
/** Map a persisted setting value to its menu cycle label (directions and speeds only; colors pass through). */
|
|
58
126
|
function toCycleValue(stored: unknown): string {
|
|
59
127
|
if (stored === "ltr") return "Left to Right";
|
|
60
128
|
if (stored === "rtl") return "Right to Left";
|
|
129
|
+
if (stored === "slow") return "Slow";
|
|
130
|
+
if (stored === "normal") return "Normal";
|
|
131
|
+
if (stored === "fast") return "Fast";
|
|
61
132
|
return String(stored);
|
|
62
133
|
}
|
|
63
134
|
|
|
@@ -65,10 +136,25 @@ function toCycleValue(stored: unknown): string {
|
|
|
65
136
|
function fromCycleValue(value: string): string {
|
|
66
137
|
if (value === "Left to Right") return "ltr";
|
|
67
138
|
if (value === "Right to Left") return "rtl";
|
|
139
|
+
if (value === "Slow") return "slow";
|
|
140
|
+
if (value === "Normal") return "normal";
|
|
141
|
+
if (value === "Fast") return "fast";
|
|
68
142
|
return value;
|
|
69
143
|
}
|
|
70
144
|
|
|
71
|
-
|
|
145
|
+
/** Convert a persisted or menu direction to a safe preview direction. */
|
|
146
|
+
export function fromCycleDirection(value: unknown): "ltr" | "rtl" {
|
|
147
|
+
const stored = typeof value === "string" ? fromCycleValue(value) : "";
|
|
148
|
+
return stored === "rtl" ? "rtl" : "ltr";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Convert a persisted or menu speed to a safe preview speed. */
|
|
152
|
+
export function fromCycleSpeed(value: unknown): "slow" | "normal" | "fast" {
|
|
153
|
+
const stored = typeof value === "string" ? fromCycleValue(value) : "";
|
|
154
|
+
return stored === "slow" || stored === "fast" ? stored : "normal";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type MenuSectionName = "User Prompt" | "“Working” Loader" | "Completion Marker" | "Options";
|
|
72
158
|
type DecorationSettings = DecoratorSettings["decorations"];
|
|
73
159
|
type FeatureSettings = DecoratorSettings["features"];
|
|
74
160
|
type DecorationBooleanKey = { [Key in keyof DecorationSettings]: DecorationSettings[Key] extends boolean ? Key : never }[keyof DecorationSettings];
|
|
@@ -78,24 +164,36 @@ type FeatureMenuEntry = MenuEntryBase & { group: "features"; key: keyof FeatureS
|
|
|
78
164
|
type MenuEntry = DecorationMenuEntry | FeatureMenuEntry;
|
|
79
165
|
export const MENU_ENTRIES: readonly MenuEntry[] = [
|
|
80
166
|
{ id: "decorateUserPrompt", label: "High-vis prompt", section: "User Prompt", group: "decorations", key: "decorateUserPrompt" },
|
|
81
|
-
{ id: "
|
|
167
|
+
{ id: "borderStyle", label: "Border style", section: "User Prompt", group: "decorations", key: "borderStyle", cycleValues: BORDER_STYLE_VALUES, cycleEnabledBy: "borderStyleEnabled", cycleDisabledValue: "double" },
|
|
168
|
+
{ id: "borderColor", label: "Border color", section: "User Prompt", group: "decorations", key: "borderColor", cycleValues: SETTING_COLOR_VALUES, cycleEnabledBy: "borderColorEnabled", cycleDisabledValue: "borderAccent" },
|
|
82
169
|
{ id: "promptIcon", label: "Pi icon", section: "User Prompt", group: "decorations", key: "promptIcon" },
|
|
83
170
|
{ id: "promptTimestamp", label: "Timestamp", section: "User Prompt", group: "decorations", key: "promptTimestamp" },
|
|
84
|
-
{ id: "
|
|
85
|
-
{ id: "
|
|
86
|
-
{ id: "
|
|
87
|
-
{ id: "
|
|
88
|
-
{ id: "
|
|
89
|
-
{ id: "
|
|
90
|
-
{ id: "
|
|
91
|
-
{ id: "
|
|
92
|
-
{ id: "
|
|
93
|
-
{ id: "
|
|
94
|
-
{ id: "
|
|
171
|
+
{ id: "promptProvider", label: "Provider", section: "User Prompt", group: "decorations", key: "promptProvider" },
|
|
172
|
+
{ id: "promptModel", label: "Model", section: "User Prompt", group: "decorations", key: "promptModel" },
|
|
173
|
+
{ id: "animatedSpinner", label: "Animated spinner", section: "“Working” Loader", group: "decorations", key: "animatedSpinner" },
|
|
174
|
+
{ id: "spinnerColor", label: "Animated spinner color", section: "“Working” Loader", group: "decorations", key: "spinnerColor", cycleValues: SETTING_COLOR_VALUES, cycleEnabledBy: "spinnerColorEnabled", cycleDisabledValue: "accent" },
|
|
175
|
+
{ id: "substituteDefaultMessage", label: "Randomize “Working” text", section: "“Working” Loader", group: "features", key: "substituteDefaultMessage" },
|
|
176
|
+
{ id: "shimmer", label: "Text shimmer", section: "“Working” Loader", group: "decorations", key: "shimmer" },
|
|
177
|
+
{ id: "shimmerInverted", label: "Invert shimmer", section: "“Working” Loader", group: "decorations", key: "shimmerInverted" },
|
|
178
|
+
{ id: "shimmerDirection", label: "Text shimmer direction", section: "“Working” Loader", group: "decorations", key: "shimmerDirection", cycleValues: ["Left to Right", "Right to Left"], cycleEnabledBy: "shimmerDirectionEnabled", cycleDisabledValue: "Left to Right" },
|
|
179
|
+
{ id: "shimmerSpeed", label: "Text shimmer speed", section: "“Working” Loader", group: "decorations", key: "shimmerSpeed", cycleValues: ["Slow", "Normal", "Fast"], cycleEnabledBy: "shimmerSpeedEnabled", cycleDisabledValue: "Normal" },
|
|
180
|
+
{ id: "tokenActivityMonitor", label: "Token activity monitor", section: "“Working” Loader", group: "decorations", key: "tokenActivityMonitor" },
|
|
181
|
+
{ id: "meterColor", label: "Token activity monitor color", section: "“Working” Loader", group: "decorations", key: "meterColor", cycleValues: SETTING_COLOR_VALUES, cycleEnabledBy: "meterColorEnabled", cycleDisabledValue: "accent" },
|
|
182
|
+
{ id: "meterDirection", label: "Token activity monitor direction", section: "“Working” Loader", group: "decorations", key: "meterDirection", cycleValues: ["Left to Right", "Right to Left"], cycleEnabledBy: "meterDirectionEnabled", cycleDisabledValue: "Right to Left" },
|
|
183
|
+
{ id: "meterDimmed", label: "Token activity monitor dimmed", section: "“Working” Loader", group: "decorations", key: "meterDimmed" },
|
|
184
|
+
{ id: "elapsedTime", label: "Elapsed time since prompt", section: "“Working” Loader", group: "features", key: "elapsedTime" },
|
|
185
|
+
{ id: "outputTokens", label: "Show output tokens", section: "“Working” Loader", group: "features", key: "outputTokens" },
|
|
186
|
+
// id differs from key: the Elements Order row already owns "tokenRate" in the menu's shared value namespace.
|
|
187
|
+
{ id: "showTokenRate", label: "Token rate", section: "“Working” Loader", group: "features", key: "tokenRate" },
|
|
188
|
+
{ id: "tokenRateColor", label: "Token rate color", section: "“Working” Loader", group: "decorations", key: "tokenRateColor", cycleValues: SETTING_COLOR_VALUES },
|
|
189
|
+
{ id: "tokenRateDimmed", label: "Token rate dimmed", section: "“Working” Loader", group: "decorations", key: "tokenRateDimmed" },
|
|
95
190
|
{ id: "doneMarker", label: "Show completion marker", section: "Completion Marker", group: "features", key: "doneMarker" },
|
|
191
|
+
{ id: "doneMarkerBorderStyle", label: "Border style", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderStyle", cycleValues: DONE_MARKER_BORDER_STYLE_VALUES },
|
|
192
|
+
{ id: "doneMarkerBorderColor", label: "Border color", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderColor", cycleValues: SETTING_COLOR_VALUES },
|
|
96
193
|
{ id: "doneMarkerIcon", label: "Pi icon", section: "Completion Marker", group: "features", key: "doneMarkerIcon" },
|
|
97
194
|
{ id: "randomizeDoneMarker", label: "Randomize “Worked” text", section: "Completion Marker", group: "features", key: "randomizeDoneMarker" },
|
|
98
195
|
{ id: "doneMarkerTokens", label: "Tokens spent", section: "Completion Marker", group: "features", key: "doneMarkerTokens" },
|
|
196
|
+
{ id: "doneMarkerInputs", label: "Mid-turn inputs", section: "Completion Marker", group: "features", key: "doneMarkerInputs" },
|
|
99
197
|
{ id: "useNerdFont", label: "Use NerdFont icons", section: "Options", group: "decorations", key: "useNerdFont" },
|
|
100
198
|
];
|
|
101
199
|
|
|
@@ -117,20 +215,41 @@ function setDecorationCycleValue(decorations: DecorationSettings, key: keyof Dec
|
|
|
117
215
|
case "borderColor":
|
|
118
216
|
case "spinnerColor":
|
|
119
217
|
case "meterColor":
|
|
120
|
-
|
|
218
|
+
case "tokenRateColor":
|
|
219
|
+
case "doneMarkerBorderColor":
|
|
220
|
+
if (isSettingColor(stored)) decorations[key] = stored;
|
|
221
|
+
return;
|
|
222
|
+
case "borderStyle":
|
|
223
|
+
if (isBorderStyle(stored)) decorations[key] = stored;
|
|
224
|
+
return;
|
|
225
|
+
case "doneMarkerBorderStyle":
|
|
226
|
+
if (isDoneMarkerBorderStyle(stored)) decorations[key] = stored;
|
|
121
227
|
return;
|
|
122
228
|
case "shimmerDirection":
|
|
123
229
|
case "meterDirection":
|
|
124
230
|
if (stored === "ltr" || stored === "rtl") decorations[key] = stored;
|
|
125
231
|
return;
|
|
232
|
+
case "shimmerSpeed":
|
|
233
|
+
if (stored === "slow" || stored === "normal" || stored === "fast") decorations[key] = stored;
|
|
234
|
+
return;
|
|
126
235
|
default:
|
|
127
236
|
// Fail loudly if a MENU_ENTRIES cycle entry is added without a handler here.
|
|
128
237
|
throw new Error(`Unhandled cycle setting: ${String(key)}`);
|
|
129
238
|
}
|
|
130
239
|
}
|
|
131
240
|
|
|
241
|
+
function buildSection(title: MenuSectionName, settings: DecoratorSettings): MenuSection {
|
|
242
|
+
return { title, items: MENU_ENTRIES.filter(entry => entry.section === title).map(entry => menuItem(entry, settings)) };
|
|
243
|
+
}
|
|
244
|
+
|
|
132
245
|
export function buildMenuSections(settings: DecoratorSettings): MenuSection[] {
|
|
133
|
-
return [
|
|
246
|
+
return [
|
|
247
|
+
buildSection("User Prompt", settings),
|
|
248
|
+
buildSection("“Working” Loader", settings),
|
|
249
|
+
{ title: "Elements Order", items: parseLoaderOrder(settings.loaderOrder).map(id => ({ id, label: LOADER_ELEMENT_LABELS[id], value: false, reorderGroup: LOADER_ORDER_ID })) },
|
|
250
|
+
buildSection("Completion Marker", settings),
|
|
251
|
+
buildSection("Options", settings),
|
|
252
|
+
];
|
|
134
253
|
}
|
|
135
254
|
|
|
136
255
|
export function applyMenuResult(settings: DecoratorSettings, values: Record<string, boolean | string>): DecoratorSettings {
|
|
@@ -147,28 +266,39 @@ export function applyMenuResult(settings: DecoratorSettings, values: Record<stri
|
|
|
147
266
|
next.features[entry.key] = value;
|
|
148
267
|
}
|
|
149
268
|
}
|
|
269
|
+
if (typeof values[LOADER_ORDER_ID] === "string") next.loaderOrder = parseLoaderOrder(values[LOADER_ORDER_ID]);
|
|
150
270
|
return next;
|
|
151
271
|
}
|
|
152
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Read settings.json, merging valid leaves over the defaults and falling back
|
|
275
|
+
* to defaults on any read/parse error. When a cycle setting's enabled flag is
|
|
276
|
+
* off, its stored value resets to the menu's disabled default so stale choices
|
|
277
|
+
* don't resurface on re-enable.
|
|
278
|
+
*/
|
|
153
279
|
export function loadSettings(): DecoratorSettings {
|
|
154
280
|
try {
|
|
155
281
|
const parsed = JSON.parse(readFileSync(settingsPath(), "utf8"));
|
|
156
282
|
if (!isPlainObject(parsed)) return structuredClone(DEFAULT_SETTINGS);
|
|
157
|
-
const settings = { decorations: mergeGroup(DEFAULT_SETTINGS.decorations, parsed.decorations), features: mergeGroup(DEFAULT_SETTINGS.features, parsed.features) };
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
["shimmerDirection", "shimmerDirectionEnabled", "ltr"],
|
|
163
|
-
["meterDirection", "meterDirectionEnabled", "ltr"],
|
|
164
|
-
];
|
|
165
|
-
for (const [key, enabledKey, resetValue] of gated) {
|
|
166
|
-
if (!settings.decorations[enabledKey]) (settings.decorations as unknown as Record<string, string>)[key] = resetValue;
|
|
283
|
+
const settings = { decorations: mergeGroup(DEFAULT_SETTINGS.decorations, parsed.decorations), features: mergeGroup(DEFAULT_SETTINGS.features, parsed.features), loaderOrder: parseLoaderOrder(parsed.loaderOrder) };
|
|
284
|
+
for (const entry of MENU_ENTRIES) {
|
|
285
|
+
if (entry.group === "decorations" && entry.cycleEnabledBy && entry.cycleDisabledValue !== undefined && !settings.decorations[entry.cycleEnabledBy]) {
|
|
286
|
+
setDecorationCycleValue(settings.decorations, entry.key, entry.cycleDisabledValue);
|
|
287
|
+
}
|
|
167
288
|
}
|
|
168
289
|
return settings;
|
|
169
290
|
} catch { return structuredClone(DEFAULT_SETTINGS); }
|
|
170
291
|
}
|
|
292
|
+
|
|
171
293
|
export function saveSettings(settings: DecoratorSettings): void {
|
|
172
|
-
const path = settingsPath();
|
|
173
|
-
|
|
294
|
+
const path = settingsPath();
|
|
295
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
296
|
+
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
297
|
+
try {
|
|
298
|
+
writeFileSync(tmpPath, `${JSON.stringify(settings, null, 2)}\n`, { flag: "wx" });
|
|
299
|
+
renameSync(tmpPath, path);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
try { unlinkSync(tmpPath); } catch { /* tmp file was never created */ }
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
174
304
|
}
|