From fffe358a3c3e5c25d805171df4688b041bc58b62 Mon Sep 17 00:00:00 2001 From: Jalil Arfaoui Date: Fri, 27 Feb 2026 01:49:23 +0100 Subject: [PATCH] Add Access Points section showing all APs of the connected SSID Display all access points sharing the connected network's SSID in a dedicated section between Signal and Nearby Networks. The connected AP is marked with a green checkmark icon, other APs get a spacer to keep BSSIDs aligned. Section is hidden when only one AP exists. NM duplicate BSSIDs are deduplicated by keeping the strongest signal. --- src/extension.ts | 88 +++++++++++++++++++++++++++++++++++++++++++++++- src/wifiInfo.ts | 40 ++++++++++++++++++++++ stylesheet.css | 12 ++++++- 3 files changed, 138 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 54634b4..330665a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -114,11 +114,16 @@ export default class WifiSignalPlusExtension extends Extension { private headerGenerationLabel: St.Label | null = null; private headerBandLabel: St.Label | null = null; private headerIcon: St.Icon | null = null; + private accessPointsSeparator: PopupMenu.PopupSeparatorMenuItem | null = null; + private accessPointsSection: PopupMenu.PopupMenuSection | null = null; + private accessPointsItems: PopupMenu.PopupBaseMenuItem[] = []; + private accessPointsUpdatePending = false; private nearbySeparator: PopupMenu.PopupSeparatorMenuItem | null = null; private nearbySection: PopupMenu.PopupMenuSection | null = null; private nearbyItems: NearbyNetworkCard[] = []; private nearbyUpdatePending = false; private currentConnectedSsid: string | undefined; + private currentConnectedBssid: string | undefined; private isMenuOpen = false; private enableEpoch = 0; @@ -149,6 +154,7 @@ export default class WifiSignalPlusExtension extends Extension { this.stopBackgroundScanTimer(); this.stopRefreshTimer(); this.wifiService?.unwatchDeviceSignals(); + this.clearAccessPointsItems(); this.clearNearbyItems(); this.indicator?.destroy(); this.wifiService?.destroy(); @@ -164,12 +170,17 @@ export default class WifiSignalPlusExtension extends Extension { this.headerGenerationLabel = null; this.headerBandLabel = null; this.headerIcon = null; + this.accessPointsSeparator = null; + this.accessPointsSection = null; + this.accessPointsItems = []; + this.accessPointsUpdatePending = false; this.nearbySeparator = null; this.nearbySection = null; this.nearbyItems = []; this.refreshPending = false; this.nearbyUpdatePending = false; this.currentConnectedSsid = undefined; + this.currentConnectedBssid = undefined; this.isMenuOpen = false; } @@ -231,6 +242,13 @@ export default class WifiSignalPlusExtension extends Extension { } }); + this.accessPointsSeparator = new PopupMenu.PopupSeparatorMenuItem('Access Points'); + this.accessPointsSeparator.visible = false; + menu.addMenuItem(this.accessPointsSeparator); + this.accessPointsSection = new PopupMenu.PopupMenuSection(); + this.accessPointsSection.actor.visible = false; + menu.addMenuItem(this.accessPointsSection); + this.nearbySeparator = new PopupMenu.PopupSeparatorMenuItem('Nearby Networks'); menu.addMenuItem(this.nearbySeparator); @@ -447,10 +465,12 @@ export default class WifiSignalPlusExtension extends Extension { if (!this.wifiService) return; this.currentConnectedSsid = isConnected(info) ? info.ssid : undefined; + this.currentConnectedBssid = isConnected(info) ? info.bssid : undefined; this.updateIndicatorLabel(info); this.updateMenuContent(info); if (this.isMenuOpen) { + await this.updateAccessPoints(); await this.updateNearbyNetworks(); } } finally { @@ -518,6 +538,9 @@ export default class WifiSignalPlusExtension extends Extension { } } + this.clearAccessPointsItems(); + this.setAccessPointsVisible(false); + this.signalHistory.length = 0; this.signalGraph?.queue_repaint(); } @@ -606,6 +629,56 @@ export default class WifiSignalPlusExtension extends Extension { return `${signalStrength} dBm (${quality})`; } + private async updateAccessPoints(): Promise { + if (!this.wifiService || !this.accessPointsSection || this.accessPointsUpdatePending) return; + + if (!this.currentConnectedSsid) { + this.clearAccessPointsItems(); + this.setAccessPointsVisible(false); + return; + } + + this.accessPointsUpdatePending = true; + let accessPoints: ScannedNetwork[]; + try { + accessPoints = await this.wifiService.getAccessPointsForSsid(this.currentConnectedSsid); + } finally { + this.accessPointsUpdatePending = false; + } + + this.clearAccessPointsItems(); + + if (accessPoints.length <= 1) { + this.setAccessPointsVisible(false); + return; + } + + this.setAccessPointsVisible(true); + + for (const ap of accessPoints) { + const isActive = ap.bssid === this.currentConnectedBssid?.toLowerCase(); + const row = this.createApRow(ap, isActive ? 'connected' : 'spacer'); + this.accessPointsSection.addMenuItem(row); + this.accessPointsItems.push(row); + } + } + + private setAccessPointsVisible(visible: boolean): void { + if (this.accessPointsSeparator) { + this.accessPointsSeparator.visible = visible; + } + if (this.accessPointsSection) { + this.accessPointsSection.actor.visible = visible; + } + } + + private clearAccessPointsItems(): void { + for (const item of this.accessPointsItems) { + item.destroy(); + } + this.accessPointsItems = []; + } + private async updateNearbyNetworks(): Promise { if (!this.wifiService || !this.nearbySection || this.nearbyUpdatePending) return; @@ -744,10 +817,23 @@ export default class WifiSignalPlusExtension extends Extension { return box; } - private createApRow(ap: ScannedNetwork): PopupMenu.PopupBaseMenuItem { + private createApRow(ap: ScannedNetwork, connectedIndicator: 'connected' | 'spacer' | 'none' = 'none'): PopupMenu.PopupBaseMenuItem { const item = new PopupMenu.PopupBaseMenuItem({ reactive: false }); item.add_style_class_name('wifi-nearby-ap'); + if (connectedIndicator === 'connected') { + const connectedIcon = new St.Icon({ + icon_name: 'emblem-ok-symbolic', + icon_size: 12, + style_class: 'wifi-ap-connected-icon', + y_align: Clutter.ActorAlign.CENTER, + }); + item.add_child(connectedIcon); + } else if (connectedIndicator === 'spacer') { + const spacer = new St.Widget({ style_class: 'wifi-ap-icon-spacer' }); + item.add_child(spacer); + } + const outerBox = new St.BoxLayout({ vertical: true, x_expand: true }); // Info row: BSSID + details + signal% diff --git a/src/wifiInfo.ts b/src/wifiInfo.ts index 483bca8..c51285a 100644 --- a/src/wifiInfo.ts +++ b/src/wifiInfo.ts @@ -139,6 +139,46 @@ export class WifiInfoService { }); } + async getAccessPointsForSsid(ssid: string): Promise { + if (!this.client) return []; + + const wifiDevice = this.findWifiDevice(); + if (!wifiDevice) return []; + + const accessPoints = wifiDevice.get_access_points(); + const bestByBssid = new Map(); + + for (const ap of accessPoints) { + const apSsid = this.decodeSsid(ap.get_ssid()); + if (apSsid !== ssid) continue; + + const bssid = (ap.get_bssid() ?? '').toLowerCase(); + if (!bssid) continue; + + const strength = ap.get_strength(); + const existing = bestByBssid.get(bssid); + if (existing && (existing.signalPercent as number) >= strength) continue; + + const frequency = asFrequencyMHz(ap.get_frequency()); + const generation = this.generationMap.get(bssid) ?? WIFI_GENERATIONS.UNKNOWN; + + bestByBssid.set(bssid, Object.freeze({ + ssid: apSsid, + bssid, + frequency, + channel: frequencyToChannel(frequency), + band: frequencyToBand(frequency), + bandwidth: getApBandwidth(ap), + maxBitrate: asBitrateMbps(ap.get_max_bitrate() / 1000), + signalPercent: asSignalPercent(strength), + security: getSecurityProtocol(ap), + generation, + })); + } + + return sortBySignalStrength([...bestByBssid.values()]); + } + async getAvailableNetworks(excludeSsid?: string): Promise> { if (!this.client) return new Map(); diff --git a/stylesheet.css b/stylesheet.css index 3052430..1dffcce 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -184,10 +184,20 @@ color: #e01b24; } +/* Access Points - connected AP icon */ +.wifi-ap-connected-icon { + color: #33d17a; +} + +/* Access Points - spacer matching icon width for non-connected rows */ +.wifi-ap-icon-spacer { + width: 12px; +} + /* Nearby networks - AP sub-rows */ .wifi-nearby-ap { min-height: 0; - padding: 3px 8px 3px 30px; + padding: 3px 8px 3px 12px; } .wifi-nearby-ap-bssid {