• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / vscode-brightscript-language / 26235216720

21 May 2026 03:18PM UTC coverage: 56.569% (+1.1%) from 55.501%
26235216720

Pull #790

github

web-flow
Merge 73903c9d3 into b9f6aae1a
Pull Request #790: Add filter dropdown to the Devices view

2291 of 4457 branches covered (51.4%)

Branch coverage included in aggregate %.

62 of 65 new or added lines in 3 files covered. (95.38%)

486 existing lines in 6 files now uncovered.

3612 of 5978 relevant lines covered (60.42%)

39.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.36
/src/viewProviders/DevicesViewProvider.ts
1
import * as vscode from 'vscode';
1✔
2
import * as semver from 'semver';
1✔
3
import type { ConfiguredDevice, DeviceManager, RokuDevice } from '../deviceDiscovery/DeviceManager';
4
import type { CredentialStore } from '../managers/CredentialStore';
5
import { util } from '../util';
1✔
6
import { ViewProviderId } from './ViewProviderId';
1✔
7

8
/**
9
 * A sequence used to generate unique IDs for tree items that don't care about having a key
10
 */
11
let treeItemKeySequence = 0;
1✔
12

13
/**
14
 * URI scheme used for device tree items to enable FileDecorationProvider
15
 */
16
const DEVICE_URI_SCHEME = 'roku-device';
1✔
17

18
/**
19
 * workspaceState key for the user's selected filters in the Devices view
20
 */
21
const DEVICES_VIEW_FILTERS_STATE_KEY = 'brightscript.devicesView.filters';
1✔
22

23
/**
24
 * Context key prefix VS Code reads to decide which menu entry (checked vs
25
 * unchecked) to show for each filter facet in the title-bar submenu.
26
 */
27
const DEVICES_VIEW_FILTER_CONTEXT_KEY_PREFIX = 'brightscript.devicesView.filter.';
1✔
28

29
export interface DevicesViewFilters {
30
    devModeEnabled: boolean;
31
    devModeDisabled: boolean;
32
    tv: boolean;
33
    setTopBox: boolean;
34
    stick: boolean;
35
    online: boolean;
36
    offline: boolean;
37
    userDefined: boolean;
38
    autoDetected: boolean;
39
}
40

41
export const DEVICES_VIEW_FILTER_KEYS: Array<keyof DevicesViewFilters> = [
1✔
42
    'devModeEnabled',
43
    'devModeDisabled',
44
    'tv',
45
    'setTopBox',
46
    'stick',
47
    'online',
48
    'offline',
49
    'userDefined',
50
    'autoDetected'
51
];
52

53
const DEFAULT_FILTERS: DevicesViewFilters = {
1✔
54
    devModeEnabled: true,
55
    devModeDisabled: false,
56
    tv: true,
57
    setTopBox: true,
58
    stick: true,
59
    online: true,
60
    offline: false,
61
    userDefined: true,
62
    autoDetected: true
63
};
64

65
export class DevicesViewProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
1✔
66

67
    public readonly id = ViewProviderId.devicesView;
23✔
68

69
    private decorationProvider: DeviceDecorationProvider;
70

71
    private filters: DevicesViewFilters;
72

73
    constructor(
74
        private deviceManager: DeviceManager,
23✔
75
        private credentialStore: CredentialStore,
23✔
76
        private context: vscode.ExtensionContext
23✔
77
    ) {
78
        this.filters = this.loadFilters();
23✔
79
        void this.pushFilterContextKeys();
23✔
80

81
        this.decorationProvider = new DeviceDecorationProvider();
23✔
82
        vscode.window.registerFileDecorationProvider(this.decorationProvider);
23✔
83

84
        // Pre-populate devices and decorations so they're ready before first render
85
        this.devices = this.deviceManager.getDevicesForUI();
23✔
86
        this.decorationProvider.updateDevices(this.devices);
23✔
87

88
        this.deviceManager.on('devices-changed', () => {
23✔
89
            this.handleDevicesChanged();
×
90
        });
91

92
        this.deviceManager.on('scanNeeded-changed', () => {
23✔
UNCOV
93
            if (!this.visible) {
×
UNCOV
94
                return;
×
95
            }
UNCOV
96
            this.deviceManager.refresh();
×
97
        });
98

99
        // Re-render when a device's stored password changes so the Clear item appears/disappears
100
        this.credentialStore.on('changed', () => {
23✔
101
            this._onDidChangeTreeData.fire(null);
×
102
        });
103
        vscode.workspace.onDidChangeConfiguration(event => {
23✔
UNCOV
104
            if (event.affectsConfiguration('brightscript.devices')) {
×
UNCOV
105
                this._onDidChangeTreeData.fire(null);
×
106
            }
107
        });
108
    }
109

110
    private visible = false;
23✔
111
    private scanProgressResolver: (() => void) | null = null;
23✔
112

113
    public setTreeView(treeView: vscode.TreeView<vscode.TreeItem>) {
114
        treeView.onDidChangeVisibility(e => {
12✔
UNCOV
115
            this.visible = e.visible;
×
UNCOV
116
            if (!this.visible) {
×
UNCOV
117
                return;
×
118
            }
UNCOV
119
            this.deviceManager.refresh();
×
120
        });
121

122
        // Health check device when expanded (not on every getChildren/devices-changed)
123
        treeView.onDidExpandElement(e => {
12✔
UNCOV
124
            const element = e.element as DeviceTreeItem;
×
UNCOV
125
            if (element?.contextValue === 'device' && element.key) {
×
UNCOV
126
                const device = this.deviceManager.getDevice(element.key);
×
UNCOV
127
                if (device) {
×
UNCOV
128
                    this.deviceManager.healthCheckDevice(device).catch(() => { });
×
129
                }
130
            }
131
        });
132

133
        this.deviceManager.on('scan-started', () => {
12✔
134
            this.showScanProgress();
12✔
135
        });
136

137
        this.deviceManager.on('scan-ended', () => {
12✔
138
            this.endScanProgress();
12✔
139
        });
140
    }
141

142
    private showScanProgress() {
143
        // If already showing progress, don't start another
144
        if (this.scanProgressResolver) {
12!
UNCOV
145
            return;
×
146
        }
147

148
        void vscode.window.withProgress(
12✔
149
            {
150
                location: { viewId: this.id }
151
            },
152
            () => {
153
                return new Promise<void>((resolve) => {
12✔
154
                    this.scanProgressResolver = resolve;
12✔
155
                });
156
            }
157
        );
158
    }
159

160
    private endScanProgress() {
161
        if (this.scanProgressResolver) {
12!
162
            this.scanProgressResolver();
12✔
163
            this.scanProgressResolver = null;
12✔
164
        }
165
    }
166

167
    private handleDevicesChanged(): void {
UNCOV
168
        this.devices = this.deviceManager.getDevicesForUI();
×
UNCOV
169
        this.decorationProvider.updateDevices(this.devices);
×
UNCOV
170
        this._onDidChangeTreeData.fire(null);
×
171
    }
172

173
    /**
174
     * Should the unique info about a device be obfuscated (i.e. randomly modified to protect the data)?
175
     */
176
    private get isConcealDeviceInfoEnabled() {
177
        return util.getConfiguration('brightscript.deviceDiscovery').get('concealDeviceInfo') === true;
12✔
178
    }
179

180
    private devices: Array<RokuDevice>;
181

182
    async getChildren(element?: DeviceTreeItem | DeviceInfoTreeItem): Promise<DeviceTreeItem[] | DeviceInfoTreeItem[]> {
183
        if (!element) {
7!
184
            // Fetch directly if devices haven't been populated yet (avoids debounce delay on initial load)
185
            if (this.devices.length === 0) {
7!
186
                this.devices = this.deviceManager.getDevicesForUI();
×
187
                this.decorationProvider.updateDevices(this.devices);
×
188
            }
189
            if (this.devices) {
7!
190
                let items: DeviceTreeItem[] = [];
7✔
191
                const visibleDevices = this.applyFilters(this.devices);
7✔
192
                for (const device of visibleDevices) {
7✔
193
                    // Make a rook item for each device
194
                    let treeItem = new DeviceTreeItem(
12✔
195
                        this.deviceManager.getDeviceDisplayName(device),
196
                        vscode.TreeItemCollapsibleState.Collapsed,
197
                        device.key,
198
                        device.deviceInfo
199
                    );
200
                    treeItem.tooltip = `${device.ip} | ${device.deviceInfo['friendly-model-name'] || ''} - ${this.concealString(device.deviceInfo['serial-number']?.toString() || '')} | ${device.deviceInfo['user-device-location'] || ''}`;
12!
201

202
                    // Set resourceUri to enable FileDecorationProvider for text coloring
203
                    // Use the device key which is serial-based when available, IP-based as fallback
204
                    treeItem.resourceUri = vscode.Uri.parse(`${DEVICE_URI_SCHEME}:/${device.key}`);
12✔
205
                    treeItem.iconPath = this.deviceManager.getIconPath(device);
12✔
206

207
                    // Set contextValue for context menu actions
208
                    // Values: device, device-user, device-workspace, device-user-workspace
209
                    const inUser = device.configuredIn?.includes('user');
12!
210
                    const inWorkspace = device.configuredIn?.includes('workspace');
12!
211
                    let contextValue = 'device';
12✔
212
                    if (inUser && inWorkspace) {
12!
UNCOV
213
                        contextValue = 'device-user-workspace';
×
214
                    } else if (inUser) {
12!
UNCOV
215
                        contextValue = 'device-user';
×
216
                    } else if (inWorkspace) {
12!
UNCOV
217
                        contextValue = 'device-workspace';
×
218
                    }
219
                    treeItem.contextValue = contextValue;
12✔
220

221
                    items.push(treeItem);
12✔
222
                }
223

224
                // Return the created root items
225
                return items;
7✔
226
            } else {
227
                // No devices
UNCOV
228
                return [];
×
229
            }
230
        } else if (element instanceof DeviceTreeItem) {
×
231
            // Process the details of a device
232
            let result: Array<DeviceInfoTreeItem> = [];
×
233

234
            //conceal all of these unique keys
235
            const details = this.concealObject(element.details, ['udn', 'device-id', 'advertising-id', 'wifi-mac', 'ethernet-mac', 'serial-number', 'keyed-developer-id']);
×
236

UNCOV
237
            for (let [key, values] of details) {
×
UNCOV
238
                result.push(
×
239
                    this.createDeviceInfoTreeItem({
240
                        label: key,
241
                        parent: element,
242
                        collapsibleState: vscode.TreeItemCollapsibleState.None,
243
                        key: key,
244
                        //if this is one of the properties that need concealed
245
                        description: values.value?.toString(),
×
246
                        tooltip: 'Copy to clipboard',
247
                        // Prepare the copy to clipboard command
248
                        command: {
249
                            command: 'extension.brightscript.copyToClipboard',
250
                            title: 'Copy To Clipboard',
251
                            arguments: [values.originalValue]
252
                        }
253
                    })
254
                );
255
            }
256

UNCOV
257
            const device = this.deviceManager.getDevice(element.key);
×
UNCOV
258
            if (!device) {
×
UNCOV
259
                return;
×
260
            }
261

UNCOV
262
            if (device.deviceInfo?.['is-tv'] === 'true') {
×
UNCOV
263
                result.unshift(
×
264
                    this.createDeviceInfoTreeItem({
265
                        label: '📺 Switch TV Input',
266
                        parent: element,
267
                        collapsibleState: vscode.TreeItemCollapsibleState.None,
268
                        description: 'click to change',
269
                        tooltip: 'Change the current TV input',
270
                        command: {
271
                            command: 'extension.brightscript.changeTvInput',
272
                            title: 'Switch TV Input',
273
                            arguments: [device.ip]
274
                        }
275
                    })
276
                );
277
            }
278

UNCOV
279
            result.unshift(
×
280
                this.createDeviceInfoTreeItem({
281
                    label: '📷 Capture Screenshot',
282
                    parent: element,
283
                    collapsibleState: vscode.TreeItemCollapsibleState.None,
284
                    tooltip: 'Capture a screenshot',
285
                    command: {
286
                        command: 'extension.brightscript.captureScreenshot',
287
                        title: 'Capture Screenshot',
288
                        arguments: [device.ip]
289
                    }
290
                })
291
            );
292

UNCOV
293
            result.unshift(
×
294
                this.createDeviceInfoTreeItem({
295
                    label: '⭐ Set as Active Device',
296
                    parent: element,
297
                    collapsibleState: vscode.TreeItemCollapsibleState.None,
298
                    tooltip: 'Set as active device',
299
                    command: {
300
                        command: 'extension.brightscript.setActiveDevice',
301
                        title: 'Set Active Device',
302
                        arguments: [device.ip]
303
                    }
304
                })
305
            );
306

UNCOV
307
            if (device.serialNumber) {
×
UNCOV
308
                const hasPassword = await this.hasStoredPasswordForSerial(device.serialNumber);
×
UNCOV
309
                if (hasPassword) {
×
UNCOV
310
                    result.unshift(
×
311
                        this.createDeviceInfoTreeItem({
312
                            label: '🗑️ Clear Device Password',
313
                            parent: element,
314
                            collapsibleState: vscode.TreeItemCollapsibleState.None,
315
                            tooltip: 'Clear the stored developer password for this device',
316
                            command: {
317
                                command: 'extension.brightscript.clearDevicePassword',
318
                                title: 'Clear Device Password',
319
                                arguments: [device.serialNumber]
320
                            }
321
                        })
322
                    );
323
                }
UNCOV
324
                result.unshift(
×
325
                    this.createDeviceInfoTreeItem({
326
                        label: hasPassword ? '🔑 Change Device Password' : '🔑 Set Device Password',
×
327
                        parent: element,
328
                        collapsibleState: vscode.TreeItemCollapsibleState.None,
329
                        tooltip: hasPassword ? 'Change the stored developer password for this device' : 'Set password for this device',
×
330
                        command: {
331
                            command: 'extension.brightscript.setDevicePassword',
332
                            title: hasPassword ? 'Change Device Password' : 'Set Device Password',
×
333
                            arguments: [device.serialNumber]
334
                        }
335
                    })
336
                );
337
            }
338

UNCOV
339
            if (semver.satisfies(element.details['software-version'], '>=11')) {
×
340
                // TODO: add ECP system hooks here in the future (like registry call, etc...)
UNCOV
341
                result.unshift(
×
342
                    this.createDeviceInfoTreeItem({
343
                        label: '📋 View Registry',
344
                        parent: element,
345
                        collapsibleState: vscode.TreeItemCollapsibleState.None,
346
                        tooltip: 'View the ECP Registry',
347
                        description: device.ip,
348
                        command: {
349
                            command: 'extension.brightscript.openRegistryInBrowser',
350
                            title: 'Open',
351
                            arguments: [device.ip]
352
                        }
353
                    })
354
                );
355
            }
356

UNCOV
357
            result.unshift(
×
358
                this.createDeviceInfoTreeItem({
359
                    label: '🔗 Open device web portal',
360
                    parent: element,
361
                    collapsibleState: vscode.TreeItemCollapsibleState.None,
362
                    tooltip: 'Open the web portal for this device',
363
                    description: device.ip,
364
                    command: {
365
                        command: 'extension.brightscript.openUrl',
366
                        title: 'Open',
367
                        arguments: [`http://${device.ip}`]
368
                    }
369
                })
370
            );
371

372
            // Return the device details
UNCOV
373
            return result;
×
374
        }
375
    }
376

377
    private createDeviceInfoTreeItem(options: {
378
        label: string;
379
        parent: DeviceTreeItem;
380
        collapsibleState: vscode.TreeItemCollapsibleState;
381
        key?: string;
382
        description?: string;
383
        details?: any;
384
        command?: vscode.Command;
385
        tooltip?: string;
386
    }) {
UNCOV
387
        const item = new DeviceInfoTreeItem(
×
388
            options.label,
389
            options.parent,
390
            options.collapsibleState,
391
            options.key ?? `tree-item-${treeItemKeySequence++}`,
×
392
            options.description ?? '',
×
393
            options.details ?? '',
×
394
            options.command
395
        );
396
        // Prepare the open url command
UNCOV
397
        item.tooltip = options.tooltip;
×
398
        return item;
×
399
    }
400

401
    /**
402
     * Called by VS Code to get a given element.
403
     * Currently we don't modify this element so it is just returned back.
404
     * @param element the requested element
405
     */
406
    getParent?(element: DeviceTreeItem | DeviceInfoTreeItem): vscode.ProviderResult<vscode.TreeItem> {
UNCOV
407
        return element?.parent;
×
408
    }
409

410
    /**
411
     * Called by VS Code to get a tree item for a given element.
412
     * Currently we don't modify this element so it is just returned back.
413
     * @param element the requested element
414
     */
415
    getTreeItem(element: vscode.TreeItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
UNCOV
416
        return element;
×
417
    }
418

419
    /**
420
     * Called by VS Code to resolve tool tips when not populated.
421
     * Currently we don't modify this element so it is just returned back.
422
     * @param element the requested element
423
     */
424
    resolveTreeItem?(item: vscode.TreeItem, element: vscode.TreeItem): vscode.ProviderResult<vscode.TreeItem> {
425
        return element;
×
426
    }
427

428
    private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem> = new vscode.EventEmitter<vscode.TreeItem>();
23✔
429
    public readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem> = this._onDidChangeTreeData.event;
23✔
430

431
    /**
432
     * Returns true when a password for the given serial number is stored in
433
     * the CredentialStore or in any `brightscript.devices[]` settings scope
434
     * (user, workspace, or workspace-folder). Used to decide whether the
435
     * "Clear Device Password" tree item should be shown.
436
     */
437
    private async hasStoredPasswordForSerial(serialNumber: string): Promise<boolean> {
UNCOV
438
        if (await this.credentialStore.getPassword(serialNumber)) {
×
UNCOV
439
            return true;
×
440
        }
441

UNCOV
442
        const scopeHasPassword = (devices: ConfiguredDevice[] | undefined): boolean => !!devices?.some(entry => entry.serialNumber === serialNumber && !!entry.password);
×
443

UNCOV
444
        const rootConfig = vscode.workspace.getConfiguration('brightscript');
×
UNCOV
445
        const rootInspection = rootConfig.inspect<ConfiguredDevice[]>('devices');
×
446
        if (scopeHasPassword(rootInspection?.globalValue) || scopeHasPassword(rootInspection?.workspaceValue)) {
×
447
            return true;
×
448
        }
449

UNCOV
450
        for (const folder of vscode.workspace.workspaceFolders ?? []) {
×
UNCOV
451
            const folderInspection = vscode.workspace.getConfiguration('brightscript', folder.uri).inspect<ConfiguredDevice[]>('devices');
×
452
            if (scopeHasPassword(folderInspection?.workspaceFolderValue)) {
×
453
                return true;
×
454
            }
455
        }
456

457
        return false;
×
458
    }
459

460
    private concealObject(object: Record<string, any>, secretKeys: string[]) {
461
        return util.concealObject(
×
462
            object,
463
            this.isConcealDeviceInfoEnabled ? secretKeys : []
×
464
        );
465
    }
466

467

468
    /**
469
     * Given a string, return a new string with random numbers and letters of the same size.
470
     * Returns the same value for every input for the lifetime of the current extension uptime
471
     */
472
    private concealString(value: string) {
473
        if (this.isConcealDeviceInfoEnabled) {
12!
474
            return util.concealString(value);
×
475
        } else {
476
            return value;
12✔
477
        }
478
    }
479

480
    /**
481
     * Filter the device list to only those matching every enabled facet
482
     * (form factor, connectivity, dev-mode). Unchecking any facet strictly
483
     * hides devices with that attribute.
484
     */
485
    private applyFilters(devices: RokuDevice[]): RokuDevice[] {
486
        const filters = this.filters;
7✔
487
        return devices.filter(device => {
7✔
488
            const info = device.deviceInfo ?? {};
23!
489
            const isTv = info['is-tv'] === 'true';
23✔
490
            const isStick = info['is-stick'] === 'true';
23✔
491
            const isSetTopBox = !isTv && !isStick;
23✔
492
            if (isTv && !filters.tv) {
23✔
493
                return false;
1✔
494
            }
495
            if (isStick && !filters.stick) {
22!
NEW
496
                return false;
×
497
            }
498
            if (isSetTopBox && !filters.setTopBox) {
22!
NEW
499
                return false;
×
500
            }
501

502
            // A device is "effectively online" when it's actually online, or when it's mid-handshake
503
            // (pending) and was online the last time we checked. A pending device with no prior online
504
            // result is treated as offline so first-load probing doesn't briefly show unverified devices.
505
            const isEffectivelyOnline = device.deviceState === 'online' ||
22✔
506
                (device.deviceState === 'pending' && device.lastDeviceState === 'online');
507
            if (!isEffectivelyOnline && !filters.offline) {
22✔
508
                return false;
3✔
509
            }
510
            if (isEffectivelyOnline && !filters.online) {
19✔
511
                return false;
2✔
512
            }
513

514
            // developer-enabled may be missing on older firmware or before the first health check;
515
            // treat unknown as enabled so we don't hide working devices.
516
            const devEnabled = info['developer-enabled'] !== 'false';
17✔
517
            if (devEnabled && !filters.devModeEnabled) {
17✔
518
                return false;
2✔
519
            }
520
            if (!devEnabled && !filters.devModeDisabled) {
15✔
521
                return false;
1✔
522
            }
523

524
            // A device that appears in both settings and the network scan is treated as user-defined.
525
            if (device.isConfigured && !filters.userDefined) {
14✔
526
                return false;
1✔
527
            }
528
            if (!device.isConfigured && !filters.autoDetected) {
13✔
529
                return false;
1✔
530
            }
531

532
            return true;
12✔
533
        });
534
    }
535

536
    private loadFilters(): DevicesViewFilters {
537
        const stored = this.context?.workspaceState.get<Partial<DevicesViewFilters>>(DEVICES_VIEW_FILTERS_STATE_KEY);
23!
538
        return { ...DEFAULT_FILTERS, ...(stored ?? {}) };
23✔
539
    }
540

541
    /**
542
     * Push per-facet context keys plus the aggregate hasActiveFilters key so
543
     * the title-bar submenu can choose which entry to render for each filter.
544
     */
545
    private pushFilterContextKeys(): Thenable<unknown> {
546
        const tasks: Thenable<unknown>[] = [];
27✔
547
        for (const key of DEVICES_VIEW_FILTER_KEYS) {
27✔
548
            tasks.push(vscode.commands.executeCommand('setContext', `${DEVICES_VIEW_FILTER_CONTEXT_KEY_PREFIX}${key}`, this.filters[key]));
243✔
549
        }
550
        return Promise.all(tasks);
27✔
551
    }
552

553
    /**
554
     * Flip a single filter facet, persist the new state, and refresh the tree.
555
     * Only the keys that differ from defaults are written to workspaceState — once the
556
     * user toggles a facet back to its default value, that key drops out of storage.
557
     */
558
    public async toggleFilter(key: keyof DevicesViewFilters): Promise<void> {
559
        if (!DEVICES_VIEW_FILTER_KEYS.includes(key)) {
5✔
560
            return;
1✔
561
        }
562
        this.filters = { ...this.filters, [key]: !this.filters[key] };
4✔
563

564
        const overrides: Partial<DevicesViewFilters> = {};
4✔
565
        for (const filterKey of DEVICES_VIEW_FILTER_KEYS) {
4✔
566
            if (this.filters[filterKey] !== DEFAULT_FILTERS[filterKey]) {
36✔
567
                overrides[filterKey] = this.filters[filterKey];
3✔
568
            }
569
        }
570
        const storedValue = Object.keys(overrides).length > 0 ? overrides : undefined;
4✔
571
        await this.context.workspaceState.update(DEVICES_VIEW_FILTERS_STATE_KEY, storedValue);
4✔
572

573
        await this.pushFilterContextKeys();
4✔
574
        this._onDidChangeTreeData.fire(null);
4✔
575
    }
576
}
577

578

579
class DeviceTreeItem extends vscode.TreeItem {
580
    constructor(
581
        public readonly label: string,
12✔
582
        public readonly collapsibleState: vscode.TreeItemCollapsibleState,
12✔
583
        public readonly key: string,
12✔
584
        public readonly details?: any,
12✔
585
        public command?: vscode.Command
12✔
586
    ) {
587
        super(label, collapsibleState);
12✔
588
    }
589

590
    public readonly parent = null;
12✔
591
}
592
class DeviceInfoTreeItem extends vscode.TreeItem {
593
    constructor(
UNCOV
594
        public readonly label: string,
×
UNCOV
595
        public readonly parent: DeviceTreeItem,
×
596
        public readonly collapsibleState: vscode.TreeItemCollapsibleState,
×
UNCOV
597
        public readonly key: string,
×
UNCOV
598
        public readonly description: string,
×
UNCOV
599
        public readonly details?: any,
×
UNCOV
600
        public command?: vscode.Command
×
601
    ) {
UNCOV
602
        super(label, collapsibleState);
×
603
    }
604
}
605

606
/**
607
 * Provides file decorations for device tree items to color text based on device state
608
 */
609
class DeviceDecorationProvider implements vscode.FileDecorationProvider {
610
    private _onDidChangeFileDecorations = new vscode.EventEmitter<vscode.Uri | vscode.Uri[]>();
23✔
611
    readonly onDidChangeFileDecorations = this._onDidChangeFileDecorations.event;
23✔
612

613
    private deviceStates = new Map<string, string>();
23✔
614

615
    updateDevices(devices: RokuDevice[]): void {
616
        const changedUris: vscode.Uri[] = [];
23✔
617
        for (const device of devices) {
23✔
618
            const oldState = this.deviceStates.get(device.key);
23✔
619
            if (oldState !== device.deviceState) {
23!
620
                this.deviceStates.set(device.key, device.deviceState);
23✔
621
                changedUris.push(vscode.Uri.parse(`${DEVICE_URI_SCHEME}:/${device.key}`));
23✔
622
            }
623
        }
624
        if (changedUris.length > 0) {
23✔
625
            this._onDidChangeFileDecorations.fire(changedUris);
7✔
626
        }
627
    }
628

629
    provideFileDecoration(uri: vscode.Uri): vscode.FileDecoration | undefined {
UNCOV
630
        if (uri.scheme !== DEVICE_URI_SCHEME) {
×
UNCOV
631
            return undefined;
×
632
        }
633

UNCOV
634
        const deviceKey = uri.path.slice(1); // Remove leading slash (key is "s:..." or "i:...")
×
UNCOV
635
        const state = this.deviceStates.get(deviceKey);
×
636

UNCOV
637
        if (state !== 'online') {
×
UNCOV
638
            return {
×
639
                color: new vscode.ThemeColor('disabledForeground')
640
            };
641
        }
642

UNCOV
643
        return undefined;
×
644
    }
645
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc