statusbar-quick-actions 0.0.10

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.
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Configuration management for StatusBar Quick Actions
3
+ */
4
+
5
+ import * as vscode from "vscode";
6
+ import {
7
+ StatusBarButtonConfig,
8
+ ExtensionConfig,
9
+ ThemeConfig,
10
+ NotificationConfig,
11
+ CommandHistoryEntry,
12
+ PresetConfig,
13
+ PresetApplicationMode,
14
+ } from "./types";
15
+
16
+ /**
17
+ * Configuration Manager
18
+ * Handles reading, writing, and watching configuration changes
19
+ */
20
+ export class ConfigManager {
21
+ private static readonly CONFIG_SECTION = "statusbarQuickActions";
22
+ private static readonly GLOBAL_STATE_KEY = "statusbarQuickActions.history";
23
+
24
+ private context: vscode.ExtensionContext | null = null;
25
+ private onChangeCallbacks: ((config: ExtensionConfig) => void)[] = [];
26
+ private configChangeListener: vscode.Disposable | null = null;
27
+
28
+ /**
29
+ * Initialize the configuration manager
30
+ */
31
+ public initialize(context: vscode.ExtensionContext): void {
32
+ this.context = context;
33
+ this.setupConfigurationWatching();
34
+ }
35
+
36
+ /**
37
+ * Get the current configuration
38
+ */
39
+ public getConfig(): ExtensionConfig {
40
+ const config = vscode.workspace.getConfiguration(
41
+ ConfigManager.CONFIG_SECTION,
42
+ );
43
+
44
+ return {
45
+ buttons: config.get("buttons", []),
46
+ theme: config.get("theme", this.getDefaultThemeConfig()),
47
+ notifications: config.get(
48
+ "notifications",
49
+ this.getDefaultNotificationConfig(),
50
+ ),
51
+ history: config.get("history", true),
52
+ autoDetect: config.get("autoDetect", true),
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Set a configuration value
58
+ */
59
+ public async setConfig<T>(key: string, value: T): Promise<void> {
60
+ const config = vscode.workspace.getConfiguration(
61
+ ConfigManager.CONFIG_SECTION,
62
+ );
63
+ await config.update(key, value, vscode.ConfigurationTarget.Workspace);
64
+ }
65
+
66
+ /**
67
+ * Get a specific configuration value
68
+ */
69
+ public getConfigValue<T>(key: string, defaultValue: T): T {
70
+ const config = vscode.workspace.getConfiguration(
71
+ ConfigManager.CONFIG_SECTION,
72
+ );
73
+ return config.get(key, defaultValue);
74
+ }
75
+
76
+ /**
77
+ * Add a callback for configuration changes
78
+ */
79
+ public onConfigurationChanged(
80
+ callback: (config: ExtensionConfig) => void,
81
+ ): vscode.Disposable {
82
+ this.onChangeCallbacks.push(callback);
83
+
84
+ return {
85
+ dispose: () => {
86
+ const index = this.onChangeCallbacks.indexOf(callback);
87
+ if (index > -1) {
88
+ this.onChangeCallbacks.splice(index, 1);
89
+ }
90
+ },
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Set up configuration change watching
96
+ */
97
+ private setupConfigurationWatching(): void {
98
+ if (!this.context) {
99
+ return;
100
+ }
101
+
102
+ this.configChangeListener = vscode.workspace.onDidChangeConfiguration(
103
+ (event) => {
104
+ if (event.affectsConfiguration(ConfigManager.CONFIG_SECTION)) {
105
+ const newConfig = this.getConfig();
106
+ this.onChangeCallbacks.forEach((callback) => {
107
+ try {
108
+ callback(newConfig);
109
+ } catch (error) {
110
+ console.error("Error in configuration change callback:", error);
111
+ }
112
+ });
113
+ }
114
+ },
115
+ );
116
+
117
+ if (this.context) {
118
+ this.context.subscriptions.push(this.configChangeListener);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Get default theme configuration
124
+ */
125
+ private getDefaultThemeConfig(): ThemeConfig {
126
+ return {
127
+ mode: "auto",
128
+ dark: {
129
+ button: {
130
+ foreground: "#ffffff",
131
+ background: "#6c757d",
132
+ },
133
+ executing: {
134
+ foreground: "#ffffff",
135
+ background: "#007acc",
136
+ },
137
+ error: {
138
+ foreground: "#ffffff",
139
+ background: "#dc3545",
140
+ },
141
+ },
142
+ light: {
143
+ button: {
144
+ foreground: "#ffffff",
145
+ background: "#6c757d",
146
+ },
147
+ executing: {
148
+ foreground: "#ffffff",
149
+ background: "#007acc",
150
+ },
151
+ error: {
152
+ foreground: "#ffffff",
153
+ background: "#dc3545",
154
+ },
155
+ },
156
+ highContrast: {
157
+ button: {
158
+ foreground: "#ffffff",
159
+ background: "#000000",
160
+ },
161
+ executing: {
162
+ foreground: "#000000",
163
+ background: "#ffff00",
164
+ },
165
+ error: {
166
+ foreground: "#ffffff",
167
+ background: "#ff0000",
168
+ },
169
+ },
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Get default notification configuration
175
+ */
176
+ private getDefaultNotificationConfig(): NotificationConfig {
177
+ return {
178
+ showSuccess: true,
179
+ showError: true,
180
+ showProgress: true,
181
+ position: "bottom-right",
182
+ duration: 5000,
183
+ includeOutput: false,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Validate configuration
189
+ */
190
+ public validateConfig(config: ExtensionConfig): {
191
+ isValid: boolean;
192
+ errors: string[];
193
+ } {
194
+ const errors: string[] = [];
195
+
196
+ // Validate buttons
197
+ if (!Array.isArray(config.buttons)) {
198
+ errors.push("Buttons must be an array");
199
+ } else {
200
+ config.buttons.forEach((button, index) => {
201
+ if (!button.id || typeof button.id !== "string") {
202
+ errors.push(`Button ${index}: ID is required and must be a string`);
203
+ }
204
+ if (!button.text && !button.icon) {
205
+ errors.push(`Button ${index}: Either text or icon is required`);
206
+ }
207
+ if (!button.command || typeof button.command !== "object") {
208
+ errors.push(
209
+ `Button ${index}: Command is required and must be an object`,
210
+ );
211
+ } else {
212
+ // Validate command structure
213
+ if (!button.command.type) {
214
+ errors.push(`Button ${index}: Command type is required`);
215
+ }
216
+ // Validate that package manager commands have a script
217
+ if (
218
+ [
219
+ "npm",
220
+ "yarn",
221
+ "pnpm",
222
+ "bun",
223
+ "bunx",
224
+ "npx",
225
+ "pnpx",
226
+ "detect",
227
+ ].includes(button.command.type) &&
228
+ !button.command.script
229
+ ) {
230
+ console.log(
231
+ `Button ${index}: ${button.command.type} command missing script`,
232
+ button.command,
233
+ );
234
+ errors.push(
235
+ `Button ${index}: Script is required for ${button.command.type} commands`,
236
+ );
237
+ }
238
+ // Validate that non-package manager commands have a command string
239
+ if (
240
+ ["shell", "github", "vscode", "task"].includes(
241
+ button.command.type,
242
+ ) &&
243
+ !button.command.command
244
+ ) {
245
+ console.log(
246
+ `Button ${index}: ${button.command.type} command missing command string`,
247
+ button.command,
248
+ );
249
+ errors.push(
250
+ `Button ${index}: Command string is required for ${button.command.type} commands`,
251
+ );
252
+ }
253
+ }
254
+ });
255
+ }
256
+
257
+ // Validate theme configuration
258
+ if (config.theme) {
259
+ if (
260
+ !["auto", "dark", "light", "highContrast"].includes(config.theme.mode)
261
+ ) {
262
+ errors.push("Theme mode must be auto, dark, light, or highContrast");
263
+ }
264
+ }
265
+
266
+ // Validate notification configuration
267
+ if (config.notifications) {
268
+ if (config.notifications.duration && config.notifications.duration < 0) {
269
+ errors.push("Notification duration must be a positive number");
270
+ }
271
+ }
272
+
273
+ return {
274
+ isValid: errors.length === 0,
275
+ errors,
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Get button configuration by ID
281
+ */
282
+ public getButtonConfig(buttonId: string): StatusBarButtonConfig | null {
283
+ const config = this.getConfig();
284
+ return config.buttons.find((button) => button.id === buttonId) || null;
285
+ }
286
+
287
+ /**
288
+ * Update a specific button configuration
289
+ */
290
+ public async updateButtonConfig(
291
+ buttonId: string,
292
+ updates: Partial<StatusBarButtonConfig>,
293
+ ): Promise<void> {
294
+ const config = this.getConfig();
295
+ const buttonIndex = config.buttons.findIndex(
296
+ (button) => button.id === buttonId,
297
+ );
298
+
299
+ if (buttonIndex === -1) {
300
+ throw new Error(`Button with ID '${buttonId}' not found`);
301
+ }
302
+
303
+ const updatedButton = { ...config.buttons[buttonIndex], ...updates };
304
+ config.buttons[buttonIndex] = updatedButton;
305
+
306
+ await this.setConfig("buttons", config.buttons);
307
+ }
308
+
309
+ /**
310
+ * Add a new button configuration
311
+ */
312
+ public async addButtonConfig(button: StatusBarButtonConfig): Promise<void> {
313
+ const config = this.getConfig();
314
+
315
+ // Check for duplicate IDs
316
+ if (config.buttons.some((b) => b.id === button.id)) {
317
+ throw new Error(`Button with ID '${button.id}' already exists`);
318
+ }
319
+
320
+ config.buttons.push(button);
321
+ await this.setConfig("buttons", config.buttons);
322
+ }
323
+
324
+ /**
325
+ * Remove a button configuration
326
+ */
327
+ public async removeButtonConfig(buttonId: string): Promise<void> {
328
+ const config = this.getConfig();
329
+ const filteredButtons = config.buttons.filter(
330
+ (button) => button.id !== buttonId,
331
+ );
332
+
333
+ if (filteredButtons.length === config.buttons.length) {
334
+ throw new Error(`Button with ID '${buttonId}' not found`);
335
+ }
336
+
337
+ await this.setConfig("buttons", filteredButtons);
338
+ }
339
+
340
+ /**
341
+ * Get command history
342
+ */
343
+ public getCommandHistory(): CommandHistoryEntry[] {
344
+ if (!this.context) {
345
+ return [];
346
+ }
347
+ return this.context.globalState.get(ConfigManager.GLOBAL_STATE_KEY, []);
348
+ }
349
+
350
+ /**
351
+ * Add command to history
352
+ */
353
+ public async addToHistory(entry: CommandHistoryEntry): Promise<void> {
354
+ if (!this.context) {
355
+ return;
356
+ }
357
+
358
+ const history = this.getCommandHistory();
359
+ history.unshift(entry);
360
+
361
+ // Keep only last 100 entries
362
+ if (history.length > 100) {
363
+ history.splice(100);
364
+ }
365
+
366
+ await this.context.globalState.update(
367
+ ConfigManager.GLOBAL_STATE_KEY,
368
+ history,
369
+ );
370
+ }
371
+
372
+ /**
373
+ * Clear command history
374
+ */
375
+ public async clearHistory(): Promise<void> {
376
+ if (!this.context) {
377
+ return;
378
+ }
379
+
380
+ await this.context.globalState.update(ConfigManager.GLOBAL_STATE_KEY, []);
381
+ }
382
+
383
+ /**
384
+ * Apply a preset to the current configuration
385
+ */
386
+ public async applyPreset(
387
+ preset: PresetConfig,
388
+ mode: PresetApplicationMode = "replace",
389
+ ): Promise<void> {
390
+ const currentConfig = this.getConfig();
391
+ let newButtons: StatusBarButtonConfig[];
392
+
393
+ switch (mode) {
394
+ case "replace":
395
+ // Replace all buttons with preset buttons
396
+ newButtons = [...preset.buttons];
397
+ break;
398
+
399
+ case "merge":
400
+ // Merge preset buttons, overwriting buttons with same ID
401
+ newButtons = [...currentConfig.buttons];
402
+ preset.buttons.forEach((presetButton) => {
403
+ const existingIndex = newButtons.findIndex(
404
+ (b) => b.id === presetButton.id,
405
+ );
406
+ if (existingIndex >= 0) {
407
+ newButtons[existingIndex] = presetButton;
408
+ } else {
409
+ newButtons.push(presetButton);
410
+ }
411
+ });
412
+ break;
413
+
414
+ case "append":
415
+ // Append preset buttons to existing buttons, ensuring unique IDs
416
+ newButtons = [...currentConfig.buttons];
417
+ preset.buttons.forEach((presetButton) => {
418
+ // Generate new ID if there's a conflict
419
+ let buttonToAdd = presetButton;
420
+ if (newButtons.some((b) => b.id === presetButton.id)) {
421
+ buttonToAdd = {
422
+ ...presetButton,
423
+ id: `${presetButton.id}_${Date.now()}`,
424
+ };
425
+ }
426
+ newButtons.push(buttonToAdd);
427
+ });
428
+ break;
429
+
430
+ default:
431
+ throw new Error(`Unknown preset application mode: ${mode}`);
432
+ }
433
+
434
+ // Update buttons configuration
435
+ await this.setConfig("buttons", newButtons);
436
+
437
+ // Apply theme if present in preset
438
+ if (preset.theme) {
439
+ await this.setConfig("theme", preset.theme);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Get buttons that would be affected by preset application
445
+ */
446
+ public getPresetImpact(
447
+ preset: PresetConfig,
448
+ mode: PresetApplicationMode,
449
+ ): {
450
+ added: number;
451
+ modified: number;
452
+ removed: number;
453
+ total: number;
454
+ } {
455
+ const currentConfig = this.getConfig();
456
+ const currentIds = new Set(currentConfig.buttons.map((b) => b.id));
457
+
458
+ let added = 0;
459
+ let modified = 0;
460
+ let removed = 0;
461
+
462
+ switch (mode) {
463
+ case "replace":
464
+ added = preset.buttons.length;
465
+ removed = currentConfig.buttons.length;
466
+ break;
467
+
468
+ case "merge":
469
+ preset.buttons.forEach((pb) => {
470
+ if (currentIds.has(pb.id)) {
471
+ modified++;
472
+ } else {
473
+ added++;
474
+ }
475
+ });
476
+ break;
477
+
478
+ case "append":
479
+ added = preset.buttons.length;
480
+ break;
481
+ }
482
+
483
+ return {
484
+ added,
485
+ modified,
486
+ removed,
487
+ total: added + modified + removed,
488
+ };
489
+ }
490
+
491
+ /**
492
+ * Validate preset before application
493
+ */
494
+ public validatePresetApplication(preset: PresetConfig): {
495
+ isValid: boolean;
496
+ errors: string[];
497
+ } {
498
+ const errors: string[] = [];
499
+
500
+ if (!preset.buttons || !Array.isArray(preset.buttons)) {
501
+ errors.push("Preset must contain a buttons array");
502
+ } else {
503
+ preset.buttons.forEach((button, index) => {
504
+ if (!button.id) {
505
+ errors.push(`Button ${index}: ID is required`);
506
+ }
507
+ if (!button.text && !button.icon) {
508
+ errors.push(`Button ${index}: Either text or icon is required`);
509
+ }
510
+ if (!button.command) {
511
+ errors.push(`Button ${index}: Command is required`);
512
+ }
513
+ });
514
+ }
515
+
516
+ return {
517
+ isValid: errors.length === 0,
518
+ errors,
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Dispose of resources
524
+ */
525
+ public dispose(): void {
526
+ if (this.configChangeListener) {
527
+ this.configChangeListener.dispose();
528
+ }
529
+ }
530
+ }