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,406 @@
1
+ /**
2
+ * Preset Management for StatusBar Quick Actions
3
+ * Handles preset storage, CRUD operations, and application
4
+ */
5
+
6
+ import * as vscode from "vscode";
7
+ import * as fs from "fs";
8
+ import { PresetConfig, PresetApplicationMode, ExtensionConfig } from "./types";
9
+
10
+ /**
11
+ * Preset Manager
12
+ * Manages configuration presets for quick button setup
13
+ */
14
+ export class PresetManager {
15
+ private static readonly PRESET_STORAGE_KEY = "statusbarQuickActions.presets";
16
+ private context: vscode.ExtensionContext | null = null;
17
+
18
+ /**
19
+ * Initialize the preset manager
20
+ */
21
+ public initialize(context: vscode.ExtensionContext): void {
22
+ this.context = context;
23
+ }
24
+
25
+ /**
26
+ * Get all presets
27
+ */
28
+ public getAllPresets(): PresetConfig[] {
29
+ if (!this.context) {
30
+ console.error("PresetManager not initialized");
31
+ return [];
32
+ }
33
+
34
+ try {
35
+ const presetsJson = this.context.globalState.get<string>(
36
+ PresetManager.PRESET_STORAGE_KEY,
37
+ "[]",
38
+ );
39
+ const presets = JSON.parse(presetsJson) as PresetConfig[];
40
+
41
+ // Convert date strings back to Date objects
42
+ return presets.map((preset) => ({
43
+ ...preset,
44
+ metadata: preset.metadata
45
+ ? {
46
+ ...preset.metadata,
47
+ created: new Date(preset.metadata.created),
48
+ modified: new Date(preset.metadata.modified),
49
+ }
50
+ : undefined,
51
+ }));
52
+ } catch (error) {
53
+ console.error("Failed to load presets:", error);
54
+ return [];
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get a preset by ID
60
+ */
61
+ public getPreset(presetId: string): PresetConfig | null {
62
+ const presets = this.getAllPresets();
63
+ return presets.find((p) => p.id === presetId) || null;
64
+ }
65
+
66
+ /**
67
+ * Save a preset
68
+ */
69
+ public async savePreset(preset: PresetConfig): Promise<void> {
70
+ if (!this.context) {
71
+ throw new Error("PresetManager not initialized");
72
+ }
73
+
74
+ try {
75
+ const presets = this.getAllPresets();
76
+ const existingIndex = presets.findIndex((p) => p.id === preset.id);
77
+
78
+ const now = new Date();
79
+ const presetWithMetadata: PresetConfig = {
80
+ ...preset,
81
+ metadata: {
82
+ created: preset.metadata?.created || now,
83
+ modified: now,
84
+ author: preset.metadata?.author,
85
+ tags: preset.metadata?.tags,
86
+ },
87
+ };
88
+
89
+ if (existingIndex >= 0) {
90
+ // Update existing preset
91
+ presets[existingIndex] = presetWithMetadata;
92
+ } else {
93
+ // Add new preset
94
+ presets.push(presetWithMetadata);
95
+ }
96
+
97
+ await this.context.globalState.update(
98
+ PresetManager.PRESET_STORAGE_KEY,
99
+ JSON.stringify(presets, null, 2),
100
+ );
101
+ } catch (error) {
102
+ const errorMessage =
103
+ error instanceof Error ? error.message : String(error);
104
+ throw new Error(`Failed to save preset: ${errorMessage}`);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Delete a preset
110
+ */
111
+ public async deletePreset(presetId: string): Promise<void> {
112
+ if (!this.context) {
113
+ throw new Error("PresetManager not initialized");
114
+ }
115
+
116
+ try {
117
+ const presets = this.getAllPresets();
118
+ const filteredPresets = presets.filter((p) => p.id !== presetId);
119
+
120
+ if (filteredPresets.length === presets.length) {
121
+ throw new Error(`Preset with ID '${presetId}' not found`);
122
+ }
123
+
124
+ await this.context.globalState.update(
125
+ PresetManager.PRESET_STORAGE_KEY,
126
+ JSON.stringify(filteredPresets, null, 2),
127
+ );
128
+ } catch (error) {
129
+ const errorMessage =
130
+ error instanceof Error ? error.message : String(error);
131
+ throw new Error(`Failed to delete preset: ${errorMessage}`);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Create a preset from current configuration
137
+ */
138
+ public async createPresetFromConfig(
139
+ name: string,
140
+ description: string,
141
+ currentConfig: ExtensionConfig,
142
+ tags?: string[],
143
+ ): Promise<PresetConfig> {
144
+ const preset: PresetConfig = {
145
+ id: `preset_${Date.now()}`,
146
+ name,
147
+ description,
148
+ buttons: currentConfig.buttons,
149
+ theme: currentConfig.theme,
150
+ metadata: {
151
+ created: new Date(),
152
+ modified: new Date(),
153
+ tags,
154
+ },
155
+ };
156
+
157
+ await this.savePreset(preset);
158
+ return preset;
159
+ }
160
+
161
+ /**
162
+ * Apply a preset to current configuration
163
+ */
164
+ public applyPreset(
165
+ preset: PresetConfig,
166
+ currentConfig: ExtensionConfig,
167
+ mode: PresetApplicationMode = "replace",
168
+ ): ExtensionConfig {
169
+ let newConfig: ExtensionConfig;
170
+
171
+ switch (mode) {
172
+ case "replace":
173
+ // Replace all buttons with preset buttons
174
+ newConfig = {
175
+ ...currentConfig,
176
+ buttons: [...preset.buttons],
177
+ };
178
+ break;
179
+
180
+ case "merge": {
181
+ // Merge preset buttons, overwriting buttons with same ID
182
+ const mergedButtons = [...currentConfig.buttons];
183
+ preset.buttons.forEach((presetButton) => {
184
+ const existingIndex = mergedButtons.findIndex(
185
+ (b) => b.id === presetButton.id,
186
+ );
187
+ if (existingIndex >= 0) {
188
+ mergedButtons[existingIndex] = presetButton;
189
+ } else {
190
+ mergedButtons.push(presetButton);
191
+ }
192
+ });
193
+ newConfig = {
194
+ ...currentConfig,
195
+ buttons: mergedButtons,
196
+ };
197
+ break;
198
+ }
199
+
200
+ case "append": {
201
+ // Append preset buttons to existing buttons
202
+ const appendedButtons = [...currentConfig.buttons, ...preset.buttons];
203
+ newConfig = {
204
+ ...currentConfig,
205
+ buttons: appendedButtons,
206
+ };
207
+ break;
208
+ }
209
+
210
+ default:
211
+ throw new Error(`Unknown preset application mode: ${mode}`);
212
+ }
213
+
214
+ // Apply theme if present in preset
215
+ if (preset.theme) {
216
+ newConfig.theme = preset.theme;
217
+ }
218
+
219
+ return newConfig;
220
+ }
221
+
222
+ /**
223
+ * Export preset to JSON file
224
+ */
225
+ public async exportPreset(preset: PresetConfig): Promise<void> {
226
+ const uri = await vscode.window.showSaveDialog({
227
+ filters: { JSON: ["json"] },
228
+ defaultUri: vscode.Uri.file(`${preset.name}.preset.json`),
229
+ });
230
+
231
+ if (!uri) {
232
+ return;
233
+ }
234
+
235
+ try {
236
+ fs.writeFileSync(uri.fsPath, JSON.stringify(preset, null, 2));
237
+ vscode.window.showInformationMessage(
238
+ `✅ Preset "${preset.name}" exported to ${uri.fsPath}`,
239
+ );
240
+ } catch (error) {
241
+ const errorMessage =
242
+ error instanceof Error ? error.message : String(error);
243
+ throw new Error(`Failed to export preset: ${errorMessage}`);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Import preset from JSON file
249
+ */
250
+ public async importPreset(): Promise<PresetConfig | null> {
251
+ const uri = await vscode.window.showOpenDialog({
252
+ filters: { JSON: ["json"] },
253
+ canSelectMany: false,
254
+ });
255
+
256
+ if (!uri || uri.length === 0) {
257
+ return null;
258
+ }
259
+
260
+ try {
261
+ const content = fs.readFileSync(uri[0].fsPath, "utf8");
262
+ const preset = JSON.parse(content) as PresetConfig;
263
+
264
+ // Validate preset structure
265
+ if (!preset.id || !preset.name || !preset.buttons) {
266
+ throw new Error(
267
+ "Invalid preset file: missing required fields (id, name, buttons)",
268
+ );
269
+ }
270
+
271
+ // Generate new ID to avoid conflicts
272
+ preset.id = `preset_${Date.now()}`;
273
+ preset.metadata = {
274
+ created: new Date(),
275
+ modified: new Date(),
276
+ author: preset.metadata?.author,
277
+ tags: preset.metadata?.tags,
278
+ };
279
+
280
+ await this.savePreset(preset);
281
+ vscode.window.showInformationMessage(
282
+ `✅ Preset "${preset.name}" imported successfully`,
283
+ );
284
+
285
+ return preset;
286
+ } catch (error) {
287
+ const errorMessage =
288
+ error instanceof Error ? error.message : String(error);
289
+ vscode.window.showErrorMessage(
290
+ `Failed to import preset: ${errorMessage}`,
291
+ );
292
+ return null;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Duplicate a preset
298
+ */
299
+ public async duplicatePreset(presetId: string): Promise<PresetConfig | null> {
300
+ const original = this.getPreset(presetId);
301
+ if (!original) {
302
+ throw new Error(`Preset with ID '${presetId}' not found`);
303
+ }
304
+
305
+ const duplicate: PresetConfig = {
306
+ ...original,
307
+ id: `preset_${Date.now()}`,
308
+ name: `${original.name} (Copy)`,
309
+ metadata: {
310
+ created: new Date(),
311
+ modified: new Date(),
312
+ author: original.metadata?.author,
313
+ tags: original.metadata?.tags,
314
+ },
315
+ };
316
+
317
+ await this.savePreset(duplicate);
318
+ return duplicate;
319
+ }
320
+
321
+ /**
322
+ * Search presets by name or tags
323
+ */
324
+ public searchPresets(query: string): PresetConfig[] {
325
+ const presets = this.getAllPresets();
326
+ const lowerQuery = query.toLowerCase();
327
+
328
+ return presets.filter(
329
+ (preset) =>
330
+ preset.name.toLowerCase().includes(lowerQuery) ||
331
+ preset.description?.toLowerCase().includes(lowerQuery) ||
332
+ preset.metadata?.tags?.some((tag) =>
333
+ tag.toLowerCase().includes(lowerQuery),
334
+ ),
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Get preset count
340
+ */
341
+ public getPresetCount(): number {
342
+ return this.getAllPresets().length;
343
+ }
344
+
345
+ /**
346
+ * Clear all presets (with confirmation)
347
+ */
348
+ public async clearAllPresets(): Promise<void> {
349
+ if (!this.context) {
350
+ throw new Error("PresetManager not initialized");
351
+ }
352
+
353
+ await this.context.globalState.update(
354
+ PresetManager.PRESET_STORAGE_KEY,
355
+ "[]",
356
+ );
357
+ }
358
+
359
+ /**
360
+ * Validate preset configuration
361
+ */
362
+ public validatePreset(preset: PresetConfig): {
363
+ isValid: boolean;
364
+ errors: string[];
365
+ } {
366
+ const errors: string[] = [];
367
+
368
+ if (!preset.id || typeof preset.id !== "string") {
369
+ errors.push("Preset ID is required and must be a string");
370
+ }
371
+
372
+ if (!preset.name || typeof preset.name !== "string") {
373
+ errors.push("Preset name is required and must be a string");
374
+ }
375
+
376
+ if (!Array.isArray(preset.buttons)) {
377
+ errors.push("Preset buttons must be an array");
378
+ } else {
379
+ preset.buttons.forEach((button, index) => {
380
+ if (!button.id || typeof button.id !== "string") {
381
+ errors.push(`Button ${index}: ID is required and must be a string`);
382
+ }
383
+ if (!button.text && !button.icon) {
384
+ errors.push(`Button ${index}: Either text or icon is required`);
385
+ }
386
+ if (!button.command || typeof button.command !== "object") {
387
+ errors.push(
388
+ `Button ${index}: Command is required and must be an object`,
389
+ );
390
+ }
391
+ });
392
+ }
393
+
394
+ return {
395
+ isValid: errors.length === 0,
396
+ errors,
397
+ };
398
+ }
399
+
400
+ /**
401
+ * Dispose of resources
402
+ */
403
+ public dispose(): void {
404
+ // Cleanup if needed
405
+ }
406
+ }
package/src/theme.ts ADDED
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Theme management for StatusBar Quick Actions
3
+ */
4
+
5
+ import * as vscode from "vscode";
6
+ import { ThemeConfig } from "./types";
7
+
8
+ export class ThemeManager {
9
+ private context: vscode.ExtensionContext | null = null;
10
+ private currentTheme: ThemeConfig | null = null;
11
+
12
+ /**
13
+ * Initialize the theme manager
14
+ */
15
+ public async initialize(context: vscode.ExtensionContext): Promise<void> {
16
+ this.context = context;
17
+ await this.loadTheme();
18
+ this.setupThemeWatching();
19
+ }
20
+
21
+ /**
22
+ * Load theme from configuration
23
+ */
24
+ private async loadTheme(): Promise<void> {
25
+ try {
26
+ const config = vscode.workspace.getConfiguration(
27
+ "statusbarQuickActions.settings",
28
+ );
29
+ const themeConfig = config.get<ThemeConfig>("theme");
30
+
31
+ if (themeConfig) {
32
+ this.currentTheme = themeConfig as ThemeConfig;
33
+ } else {
34
+ // Use default theme if not configured
35
+ this.currentTheme = this.getDefaultTheme();
36
+ }
37
+ } catch (error) {
38
+ console.error("Error loading theme:", error);
39
+ this.currentTheme = this.getDefaultTheme();
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Apply theme to a statusbar item
45
+ */
46
+ public applyThemeToStatusBarItem(item: vscode.StatusBarItem): void {
47
+ if (!this.currentTheme) {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ const colors = this.getCurrentThemeColors();
53
+ if (colors.foreground) {
54
+ item.color = new vscode.ThemeColor(colors.foreground);
55
+ }
56
+ if (colors.background) {
57
+ item.backgroundColor = new vscode.ThemeColor(colors.background);
58
+ }
59
+ } catch (error) {
60
+ console.error("Error applying theme to statusbar item:", error);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get current theme colors
66
+ */
67
+ public getCurrentThemeColors(): { foreground?: string; background?: string } {
68
+ if (!this.currentTheme) {
69
+ return {};
70
+ }
71
+
72
+ try {
73
+ const themeType = this.getCurrentThemeType();
74
+ const theme = this.currentTheme[themeType];
75
+ if (!theme || typeof theme !== "object") {
76
+ return {};
77
+ }
78
+
79
+ return {
80
+ foreground: theme.button?.foreground,
81
+ background: theme.button?.background,
82
+ };
83
+ } catch (error) {
84
+ console.error("Error getting current theme colors:", error);
85
+ return {};
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Get current theme type (dark/light/highContrast)
91
+ */
92
+ private getCurrentThemeType(): "dark" | "light" | "highContrast" {
93
+ if (!this.currentTheme) {
94
+ return "dark";
95
+ }
96
+
97
+ const mode = this.currentTheme.mode;
98
+ if (mode === "auto") {
99
+ // Detect from VSCode theme
100
+ const colorTheme = vscode.workspace
101
+ .getConfiguration()
102
+ .get("workbench.colorTheme");
103
+ const isDark =
104
+ colorTheme?.toString().toLowerCase().includes("dark") ||
105
+ colorTheme?.toString().toLowerCase().includes("black") ||
106
+ colorTheme?.toString().toLowerCase().includes("dimmed");
107
+
108
+ // Check for high contrast
109
+ const isHighContrast =
110
+ vscode.workspace
111
+ .getConfiguration()
112
+ .get("accessibility.verbosityNotifications") === "verbose";
113
+
114
+ return isHighContrast ? "highContrast" : isDark ? "dark" : "light";
115
+ } else if (mode === "highContrast") {
116
+ return "highContrast";
117
+ } else if (mode === "dark") {
118
+ return "dark";
119
+ } else {
120
+ return "light";
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Set up theme change watching
126
+ */
127
+ private setupThemeWatching(): void {
128
+ if (!this.context) {
129
+ return;
130
+ }
131
+
132
+ // Watch for theme changes
133
+ const disposable = vscode.workspace.onDidChangeConfiguration((event) => {
134
+ if (
135
+ event.affectsConfiguration("workbench.colorTheme") ||
136
+ event.affectsConfiguration("accessibility")
137
+ ) {
138
+ this.updateTheme();
139
+ }
140
+ });
141
+
142
+ this.context.subscriptions.push(disposable);
143
+ }
144
+
145
+ /**
146
+ * Update current theme
147
+ */
148
+ private updateTheme(): void {
149
+ // Reload theme from configuration
150
+ this.loadTheme();
151
+ }
152
+
153
+ /**
154
+ * Get theme for executing state
155
+ */
156
+ public getExecutingThemeColors(): {
157
+ foreground?: string;
158
+ background?: string;
159
+ } {
160
+ if (!this.currentTheme) {
161
+ return {};
162
+ }
163
+
164
+ try {
165
+ const themeType = this.getCurrentThemeType();
166
+ const theme = this.currentTheme[themeType];
167
+ if (!theme || typeof theme !== "object") {
168
+ return {};
169
+ }
170
+
171
+ return {
172
+ foreground: theme.executing?.foreground,
173
+ background: theme.executing?.background,
174
+ };
175
+ } catch (error) {
176
+ console.error("Error getting executing theme colors:", error);
177
+ return {};
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Get theme for error state
183
+ */
184
+ public getErrorThemeColors(): { foreground?: string; background?: string } {
185
+ if (!this.currentTheme) {
186
+ return {};
187
+ }
188
+
189
+ try {
190
+ const themeType = this.getCurrentThemeType();
191
+ const theme = this.currentTheme[themeType];
192
+ if (!theme || typeof theme !== "object") {
193
+ return {};
194
+ }
195
+
196
+ return {
197
+ foreground: theme.error?.foreground,
198
+ background: theme.error?.background,
199
+ };
200
+ } catch (error) {
201
+ console.error("Error getting error theme colors:", error);
202
+ return {};
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Export theme configuration
208
+ */
209
+ public exportTheme(): ThemeConfig | null {
210
+ return this.currentTheme;
211
+ }
212
+
213
+ /**
214
+ * Import theme configuration
215
+ */
216
+ public async importTheme(theme: ThemeConfig): Promise<void> {
217
+ this.currentTheme = theme;
218
+ // Trigger theme update
219
+ this.updateTheme();
220
+ }
221
+
222
+ /**
223
+ * Reset to default theme
224
+ */
225
+ public async resetToDefault(): Promise<void> {
226
+ this.currentTheme = this.getDefaultTheme();
227
+ this.updateTheme();
228
+ }
229
+
230
+ /**
231
+ * Get default theme configuration
232
+ */
233
+ private getDefaultTheme(): ThemeConfig {
234
+ return {
235
+ mode: "auto",
236
+ dark: {
237
+ button: {
238
+ foreground: "#ffffff",
239
+ background: "#6c757d",
240
+ },
241
+ executing: {
242
+ foreground: "#ffffff",
243
+ background: "#007acc",
244
+ },
245
+ error: {
246
+ foreground: "#ffffff",
247
+ background: "#dc3545",
248
+ },
249
+ },
250
+ light: {
251
+ button: {
252
+ foreground: "#ffffff",
253
+ background: "#6c757d",
254
+ },
255
+ executing: {
256
+ foreground: "#ffffff",
257
+ background: "#007acc",
258
+ },
259
+ error: {
260
+ foreground: "#ffffff",
261
+ background: "#dc3545",
262
+ },
263
+ },
264
+ highContrast: {
265
+ button: {
266
+ foreground: "#ffffff",
267
+ background: "#000000",
268
+ },
269
+ executing: {
270
+ foreground: "#000000",
271
+ background: "#ffff00",
272
+ },
273
+ error: {
274
+ foreground: "#ffffff",
275
+ background: "#ff0000",
276
+ },
277
+ },
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Check if high contrast mode is enabled
283
+ */
284
+ public isHighContrastMode(): boolean {
285
+ return this.getCurrentThemeType() === "highContrast";
286
+ }
287
+
288
+ /**
289
+ * Get recommended color scheme for accessibility
290
+ */
291
+ public getAccessibilityColors(): { foreground: string; background: string } {
292
+ const themeType = this.getCurrentThemeType();
293
+
294
+ if (themeType === "highContrast") {
295
+ return {
296
+ foreground: "#ffffff",
297
+ background: "#000000",
298
+ };
299
+ } else if (themeType === "dark") {
300
+ return {
301
+ foreground: "#ffffff",
302
+ background: "#333333",
303
+ };
304
+ } else {
305
+ return {
306
+ foreground: "#000000",
307
+ background: "#cccccc",
308
+ };
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Dispose of resources
314
+ */
315
+ public dispose(): void {
316
+ this.context = null;
317
+ }
318
+ }