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,360 @@
1
+ /**
2
+ * Dynamic Label Management for StatusBar Quick Actions
3
+ * Handles dynamic label evaluation and refresh
4
+ */
5
+
6
+ import * as vscode from "vscode";
7
+ import * as https from "https";
8
+ import * as http from "http";
9
+ import { DynamicLabelField, DynamicLabelState } from "./types";
10
+
11
+ /**
12
+ * Dynamic Label Manager
13
+ * Evaluates and refreshes dynamic labels
14
+ */
15
+ export class DynamicLabelManager {
16
+ private labelStates = new Map<string, DynamicLabelState>();
17
+ private gitExtension: unknown = null;
18
+
19
+ /**
20
+ * Initialize the dynamic label manager
21
+ */
22
+ public async initialize(): Promise<void> {
23
+ // Try to get Git extension
24
+ try {
25
+ const gitExt = vscode.extensions.getExtension("vscode.git");
26
+ if (gitExt) {
27
+ this.gitExtension = gitExt.isActive
28
+ ? gitExt.exports
29
+ : await gitExt.activate();
30
+ }
31
+ } catch (error) {
32
+ console.warn("Git extension not available:", error);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Evaluate a dynamic label field
38
+ */
39
+ public async evaluateLabel(
40
+ buttonId: string,
41
+ field: DynamicLabelField,
42
+ ): Promise<string> {
43
+ try {
44
+ let value: string;
45
+
46
+ switch (field.type) {
47
+ case "time":
48
+ value = this.evaluateTimeLabel(field);
49
+ break;
50
+ case "url":
51
+ value = await this.evaluateUrlLabel(field);
52
+ break;
53
+ case "env":
54
+ value = this.evaluateEnvLabel(field);
55
+ break;
56
+ case "git":
57
+ value = await this.evaluateGitLabel(field);
58
+ break;
59
+ case "custom":
60
+ value = this.evaluateCustomLabel(field);
61
+ break;
62
+ default:
63
+ value = field.fallback || "N/A";
64
+ }
65
+
66
+ // Apply template if provided
67
+ if (field.template) {
68
+ value = this.applyTemplate(field.template, value);
69
+ }
70
+
71
+ // Update state
72
+ const state: DynamicLabelState = {
73
+ fieldConfig: field,
74
+ lastValue: value,
75
+ lastUpdated: new Date(),
76
+ };
77
+
78
+ // Setup refresh timer if needed
79
+ if (field.refreshInterval && field.refreshInterval > 0) {
80
+ this.setupRefreshTimer(buttonId, field);
81
+ }
82
+
83
+ this.labelStates.set(buttonId, state);
84
+
85
+ return value;
86
+ } catch (error) {
87
+ const errorMessage =
88
+ error instanceof Error ? error.message : String(error);
89
+ console.error(`Failed to evaluate dynamic label for ${buttonId}:`, error);
90
+
91
+ // Update state with error
92
+ const state: DynamicLabelState = {
93
+ fieldConfig: field,
94
+ lastValue: field.fallback || "Error",
95
+ lastUpdated: new Date(),
96
+ error: errorMessage,
97
+ };
98
+ this.labelStates.set(buttonId, state);
99
+
100
+ return field.fallback || "Error";
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Evaluate time-based label
106
+ */
107
+ private evaluateTimeLabel(field: DynamicLabelField): string {
108
+ const now = new Date();
109
+ const format = field.format || "HH:mm:ss";
110
+
111
+ try {
112
+ return this.formatDate(now, format);
113
+ } catch (error) {
114
+ console.error("Failed to format time:", error);
115
+ return now.toLocaleTimeString();
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Format date according to pattern
121
+ */
122
+ private formatDate(date: Date, format: string): string {
123
+ const pad = (n: number) => String(n).padStart(2, "0");
124
+
125
+ const replacements: Record<string, string> = {
126
+ YYYY: String(date.getFullYear()),
127
+ YY: String(date.getFullYear()).slice(-2),
128
+ MM: pad(date.getMonth() + 1),
129
+ DD: pad(date.getDate()),
130
+ HH: pad(date.getHours()),
131
+ hh: pad(date.getHours() % 12 || 12),
132
+ mm: pad(date.getMinutes()),
133
+ ss: pad(date.getSeconds()),
134
+ A: date.getHours() >= 12 ? "PM" : "AM",
135
+ a: date.getHours() >= 12 ? "pm" : "am",
136
+ };
137
+
138
+ let result = format;
139
+ for (const [pattern, value] of Object.entries(replacements)) {
140
+ result = result.replace(new RegExp(pattern, "g"), value);
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ /**
147
+ * Evaluate URL-based label
148
+ */
149
+ private async evaluateUrlLabel(field: DynamicLabelField): Promise<string> {
150
+ if (!field.url) {
151
+ throw new Error("URL is required for url-type dynamic label");
152
+ }
153
+
154
+ return new Promise((resolve, reject) => {
155
+ const url = new URL(field.url!);
156
+ const client = url.protocol === "https:" ? https : http;
157
+
158
+ const request = client.get(field.url!, (response) => {
159
+ let data = "";
160
+
161
+ response.on("data", (chunk) => {
162
+ data += chunk;
163
+ });
164
+
165
+ response.on("end", () => {
166
+ try {
167
+ // Try to parse as JSON first
168
+ const json = JSON.parse(data);
169
+ resolve(JSON.stringify(json));
170
+ } catch {
171
+ // Return raw text if not JSON
172
+ resolve(data.trim().substring(0, 100)); // Limit to 100 chars
173
+ }
174
+ });
175
+ });
176
+
177
+ request.on("error", (error) => {
178
+ reject(error);
179
+ });
180
+
181
+ request.setTimeout(5000, () => {
182
+ request.destroy();
183
+ reject(new Error("Request timeout"));
184
+ });
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Evaluate environment variable label
190
+ */
191
+ private evaluateEnvLabel(field: DynamicLabelField): string {
192
+ if (!field.envVar) {
193
+ throw new Error("Environment variable name is required");
194
+ }
195
+
196
+ const value = process.env[field.envVar];
197
+ if (value === undefined) {
198
+ throw new Error(`Environment variable '${field.envVar}' not found`);
199
+ }
200
+
201
+ return value;
202
+ }
203
+
204
+ /**
205
+ * Evaluate Git-based label
206
+ */
207
+ private async evaluateGitLabel(field: DynamicLabelField): Promise<string> {
208
+ if (!this.gitExtension) {
209
+ throw new Error("Git extension not available");
210
+ }
211
+
212
+ if (!field.gitInfo) {
213
+ throw new Error("Git info type is required");
214
+ }
215
+
216
+ try {
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ const git = (this.gitExtension as any).getAPI(1);
219
+ if (git.repositories.length === 0) {
220
+ throw new Error("No Git repository found");
221
+ }
222
+
223
+ const repo = git.repositories[0];
224
+
225
+ switch (field.gitInfo) {
226
+ case "branch":
227
+ return repo.state.HEAD?.name || "Unknown";
228
+ case "status": {
229
+ const changes =
230
+ repo.state.workingTreeChanges.length +
231
+ repo.state.indexChanges.length;
232
+ return changes > 0 ? `${changes} changes` : "Clean";
233
+ }
234
+ case "remote":
235
+ return repo.state.HEAD?.upstream?.remote || "No remote";
236
+ default:
237
+ throw new Error(`Unknown git info type: ${field.gitInfo}`);
238
+ }
239
+ } catch (error) {
240
+ const errorMessage =
241
+ error instanceof Error ? error.message : String(error);
242
+ throw new Error(`Failed to get git info: ${errorMessage}`);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Evaluate custom label (placeholder for future extension)
248
+ */
249
+ private evaluateCustomLabel(field: DynamicLabelField): string {
250
+ if (!field.customFunction) {
251
+ throw new Error("Custom function name is required");
252
+ }
253
+
254
+ // Placeholder for custom function evaluation
255
+ // In a real implementation, this could load and execute user-defined functions
256
+ throw new Error(
257
+ `Custom functions not yet implemented: ${field.customFunction}`,
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Apply template to value
263
+ */
264
+ private applyTemplate(template: string, value: string): string {
265
+ return template.replace(/\$\{value\}/g, value);
266
+ }
267
+
268
+ /**
269
+ * Setup automatic refresh timer for a label
270
+ */
271
+ private setupRefreshTimer(buttonId: string, field: DynamicLabelField): void {
272
+ // Clear existing timer if any
273
+ const existingState = this.labelStates.get(buttonId);
274
+ if (existingState?.refreshTimer) {
275
+ clearInterval(existingState.refreshTimer);
276
+ }
277
+
278
+ // Only setup timer if refresh interval is positive
279
+ if (!field.refreshInterval || field.refreshInterval <= 0) {
280
+ return;
281
+ }
282
+
283
+ // Setup new timer
284
+ const timer = setInterval(async () => {
285
+ try {
286
+ await this.evaluateLabel(buttonId, field);
287
+ // Emit event or callback to update button display
288
+ this.onLabelRefresh?.(buttonId);
289
+ } catch (error) {
290
+ console.error(`Failed to refresh label for ${buttonId}:`, error);
291
+ }
292
+ }, field.refreshInterval);
293
+
294
+ // Update state with timer
295
+ const state = this.labelStates.get(buttonId);
296
+ if (state) {
297
+ state.refreshTimer = timer;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Callback for label refresh (to be set by extension)
303
+ */
304
+ public onLabelRefresh?: (buttonId: string) => void;
305
+
306
+ /**
307
+ * Get label state
308
+ */
309
+ public getLabelState(buttonId: string): DynamicLabelState | undefined {
310
+ return this.labelStates.get(buttonId);
311
+ }
312
+
313
+ /**
314
+ * Stop refresh timer for a button
315
+ */
316
+ public stopRefreshTimer(buttonId: string): void {
317
+ const state = this.labelStates.get(buttonId);
318
+ if (state?.refreshTimer) {
319
+ clearInterval(state.refreshTimer);
320
+ state.refreshTimer = undefined;
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Stop all refresh timers
326
+ */
327
+ public stopAllRefreshTimers(): void {
328
+ for (const [buttonId] of this.labelStates) {
329
+ this.stopRefreshTimer(buttonId);
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Force refresh a label
335
+ */
336
+ public async refreshLabel(buttonId: string): Promise<string | null> {
337
+ const state = this.labelStates.get(buttonId);
338
+ if (!state) {
339
+ return null;
340
+ }
341
+
342
+ return await this.evaluateLabel(buttonId, state.fieldConfig);
343
+ }
344
+
345
+ /**
346
+ * Clear label state
347
+ */
348
+ public clearLabelState(buttonId: string): void {
349
+ this.stopRefreshTimer(buttonId);
350
+ this.labelStates.delete(buttonId);
351
+ }
352
+
353
+ /**
354
+ * Dispose of resources
355
+ */
356
+ public dispose(): void {
357
+ this.stopAllRefreshTimers();
358
+ this.labelStates.clear();
359
+ }
360
+ }