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.
- package/.github/FUNDING.yml +3 -0
- package/.vscodeignore +11 -0
- package/CLAUDE.md +230 -0
- package/LICENSE +21 -0
- package/README.md +529 -0
- package/assets/icon.png +0 -0
- package/bun.lock +908 -0
- package/docs/PERFORMANCE_OPTIMIZATIONS.md +240 -0
- package/docs/PRESET_AND_DYNAMIC_LABELS.md +536 -0
- package/docs/SAMPLE-CONFIGURATIONS.md +973 -0
- package/eslint.config.mjs +41 -0
- package/package.json +605 -0
- package/src/config-cli.ts +1287 -0
- package/src/configuration.ts +530 -0
- package/src/dynamic-label.ts +360 -0
- package/src/executor.ts +406 -0
- package/src/extension.ts +1754 -0
- package/src/history.ts +175 -0
- package/src/material-icons.ts +388 -0
- package/src/notifications.ts +189 -0
- package/src/output-panel.ts +403 -0
- package/src/preset-manager.ts +406 -0
- package/src/theme.ts +318 -0
- package/src/types.ts +368 -0
- package/src/utils/debounce.ts +91 -0
- package/src/visibility.ts +283 -0
- package/tsconfig.dev.json +10 -0
- package/tsconfig.json +19 -0
|
@@ -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
|
+
}
|