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.
This commit is contained in:
Jalil Arfaoui 2026-02-27 01:49:23 +01:00
parent 7fbced9d25
commit fffe358a3c
3 changed files with 138 additions and 2 deletions

View file

@ -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<void> {
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<void> {
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%

View file

@ -139,6 +139,46 @@ export class WifiInfoService {
});
}
async getAccessPointsForSsid(ssid: string): Promise<ScannedNetwork[]> {
if (!this.client) return [];
const wifiDevice = this.findWifiDevice();
if (!wifiDevice) return [];
const accessPoints = wifiDevice.get_access_points();
const bestByBssid = new Map<string, ScannedNetwork>();
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<Map<string, ScannedNetwork[]>> {
if (!this.client) return new Map();

View file

@ -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 {