diff --git a/package.json b/package.json
index 86ef91b..3cac7bd 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"build": "tsc && npm run copy-assets",
- "copy-assets": "cp metadata.json stylesheet.css dist/",
+ "copy-assets": "cp metadata.json stylesheet.css dist/ && cp -r src/icons dist/",
"install-extension": "npm run build && rm -rf ~/.local/share/gnome-shell/extensions/wifi-signal-plus@music-music.music && cp -r dist ~/.local/share/gnome-shell/extensions/wifi-signal-plus@music-music.music",
"nested": "npm run install-extension && MUTTER_DEBUG_DUMMY_MODE_SPECS=1920x1080 dbus-run-session gnome-shell --devkit --wayland",
"watch": "tsc --watch",
diff --git a/src/extension.ts b/src/extension.ts
index e7cb481..f4cc6c1 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -5,6 +5,7 @@
*/
import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import St from 'gi://St';
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
@@ -24,6 +25,7 @@ import {
GENERATION_CSS_CLASSES,
getGenerationLabel,
getGenerationDescription,
+ getGenerationIconFilename,
} from './wifiGeneration.js';
import type { GenerationCssClass, ChannelWidthMHz, SignalDbm } from './types.js';
@@ -69,6 +71,7 @@ const MENU_STRUCTURE: readonly MenuItemConfig[][] = [
export default class WifiSignalPlusExtension extends Extension {
private indicator: PanelMenu.Button | null = null;
+ private icon: St.Icon | null = null;
private label: St.Label | null = null;
private wifiService: WifiInfoService | null = null;
private refreshTimeout: number | null = null;
@@ -95,6 +98,7 @@ export default class WifiSignalPlusExtension extends Extension {
this.indicator = null;
this.wifiService = null;
+ this.icon = null;
this.label = null;
this.menuItems.clear();
}
@@ -103,12 +107,18 @@ export default class WifiSignalPlusExtension extends Extension {
this.indicator = new PanelMenu.Button(0.0, this.metadata.name, false);
this.indicator.add_style_class_name('wifi-signal-plus-indicator');
+ this.icon = new St.Icon({
+ style_class: 'system-status-icon',
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
this.label = new St.Label({
text: 'WiFi',
y_align: Clutter.ActorAlign.CENTER,
style_class: 'wifi-signal-plus-label',
});
+ this.indicator.add_child(this.icon);
this.indicator.add_child(this.label);
this.buildMenu();
@@ -153,17 +163,31 @@ export default class WifiSignalPlusExtension extends Extension {
}
private updateIndicatorLabel(info: WifiConnectionInfo): void {
- if (!this.label) return;
+ if (!this.icon || !this.label) return;
this.clearGenerationStyles();
if (!isConnected(info)) {
+ this.icon.visible = false;
+ this.label.visible = true;
this.label.set_text('WiFi --');
this.label.add_style_class_name(GENERATION_CSS_CLASSES[WIFI_GENERATIONS.UNKNOWN]);
return;
}
- this.label.set_text(getGenerationLabel(info.generation));
+ const iconFilename = getGenerationIconFilename(info.generation);
+ if (iconFilename) {
+ const iconPath = GLib.build_filenamev([this.path, 'icons', iconFilename]);
+ const file = Gio.File.new_for_path(iconPath);
+ this.icon.gicon = new Gio.FileIcon({ file });
+ this.icon.visible = true;
+ this.label.visible = false;
+ } else {
+ this.icon.visible = false;
+ this.label.visible = true;
+ this.label.set_text(getGenerationLabel(info.generation));
+ }
+
this.label.add_style_class_name(GENERATION_CSS_CLASSES[info.generation]);
}
diff --git a/src/icons/wifi-1.svg b/src/icons/wifi-1.svg
new file mode 100644
index 0000000..22ff6ab
--- /dev/null
+++ b/src/icons/wifi-1.svg
@@ -0,0 +1 @@
+
diff --git a/src/icons/wifi-2.svg b/src/icons/wifi-2.svg
new file mode 100644
index 0000000..45db761
--- /dev/null
+++ b/src/icons/wifi-2.svg
@@ -0,0 +1 @@
+
diff --git a/src/icons/wifi-3.svg b/src/icons/wifi-3.svg
new file mode 100644
index 0000000..2233dcb
--- /dev/null
+++ b/src/icons/wifi-3.svg
@@ -0,0 +1 @@
+
diff --git a/src/icons/wifi-4.png b/src/icons/wifi-4.png
new file mode 100644
index 0000000..9e62a95
Binary files /dev/null and b/src/icons/wifi-4.png differ
diff --git a/src/icons/wifi-5.png b/src/icons/wifi-5.png
new file mode 100644
index 0000000..c40e278
Binary files /dev/null and b/src/icons/wifi-5.png differ
diff --git a/src/icons/wifi-6.png b/src/icons/wifi-6.png
new file mode 100644
index 0000000..8cbe716
Binary files /dev/null and b/src/icons/wifi-6.png differ
diff --git a/src/icons/wifi-7.png b/src/icons/wifi-7.png
new file mode 100644
index 0000000..dca98ae
Binary files /dev/null and b/src/icons/wifi-7.png differ
diff --git a/src/types.ts b/src/types.ts
index 0e5f630..49bb545 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -17,6 +17,9 @@ export type GuardIntervalUs = Brand;
export const WIFI_GENERATIONS = {
UNKNOWN: 0,
+ WIFI_1: 1,
+ WIFI_2: 2,
+ WIFI_3: 3,
WIFI_4: 4,
WIFI_5: 5,
WIFI_6: 6,
@@ -25,11 +28,14 @@ export const WIFI_GENERATIONS = {
export type WifiGeneration = (typeof WIFI_GENERATIONS)[keyof typeof WIFI_GENERATIONS];
-export function isKnownGeneration(gen: WifiGeneration): gen is 4 | 5 | 6 | 7 {
- return gen >= WIFI_GENERATIONS.WIFI_4 && gen <= WIFI_GENERATIONS.WIFI_7;
+export function isKnownGeneration(gen: WifiGeneration): gen is 1 | 2 | 3 | 4 | 5 | 6 | 7 {
+ return gen >= WIFI_GENERATIONS.WIFI_1 && gen <= WIFI_GENERATIONS.WIFI_7;
}
export const IEEE_STANDARDS = {
+ [WIFI_GENERATIONS.WIFI_1]: '802.11b',
+ [WIFI_GENERATIONS.WIFI_2]: '802.11a',
+ [WIFI_GENERATIONS.WIFI_3]: '802.11g',
[WIFI_GENERATIONS.WIFI_4]: '802.11n',
[WIFI_GENERATIONS.WIFI_5]: '802.11ac',
[WIFI_GENERATIONS.WIFI_6]: '802.11ax',
@@ -64,10 +70,13 @@ export const SECURITY_PROTOCOLS = [
export type SecurityProtocol = (typeof SECURITY_PROTOCOLS)[number];
-export type GenerationCssClass = `wifi-gen-${4 | 5 | 6 | 7}` | 'wifi-disconnected';
+export type GenerationCssClass = `wifi-gen-${1 | 2 | 3 | 4 | 5 | 6 | 7}` | 'wifi-disconnected';
export type SignalCssClass = `wifi-signal-${Lowercase>}` | '';
export const GENERATION_CSS_CLASSES = {
+ [WIFI_GENERATIONS.WIFI_1]: 'wifi-gen-1',
+ [WIFI_GENERATIONS.WIFI_2]: 'wifi-gen-2',
+ [WIFI_GENERATIONS.WIFI_3]: 'wifi-gen-3',
[WIFI_GENERATIONS.WIFI_4]: 'wifi-gen-4',
[WIFI_GENERATIONS.WIFI_5]: 'wifi-gen-5',
[WIFI_GENERATIONS.WIFI_6]: 'wifi-gen-6',
diff --git a/src/wifiGeneration.test.ts b/src/wifiGeneration.test.ts
index 393654f..21cf4fe 100644
--- a/src/wifiGeneration.test.ts
+++ b/src/wifiGeneration.test.ts
@@ -7,6 +7,7 @@ import {
GENERATION_CSS_CLASSES,
getGenerationLabel,
getGenerationDescription,
+ getGenerationIconFilename,
isKnownGeneration,
} from './wifiGeneration';
import { GUARD_INTERVALS } from './types';
@@ -37,6 +38,9 @@ describe('createEmptyIwLinkInfo', () => {
describe('isKnownGeneration', () => {
it('should return true for known generations', () => {
+ expect(isKnownGeneration(WIFI_GENERATIONS.WIFI_1)).toBe(true);
+ expect(isKnownGeneration(WIFI_GENERATIONS.WIFI_2)).toBe(true);
+ expect(isKnownGeneration(WIFI_GENERATIONS.WIFI_3)).toBe(true);
expect(isKnownGeneration(WIFI_GENERATIONS.WIFI_4)).toBe(true);
expect(isKnownGeneration(WIFI_GENERATIONS.WIFI_5)).toBe(true);
expect(isKnownGeneration(WIFI_GENERATIONS.WIFI_6)).toBe(true);
@@ -226,8 +230,118 @@ describe('parseIwLinkOutput', () => {
});
});
+describe('legacy WiFi detection', () => {
+ describe('WiFi 2 (802.11a) - 5 GHz legacy', () => {
+ it('should detect WiFi 2 for 5 GHz without generation markers', () => {
+ const iwOutput = `Connected to aa:bb:cc:dd:ee:ff (on wlan0)
+ SSID: LegacyNetwork
+ freq: 5180
+ signal: -60 dBm
+ tx bitrate: 54.0 MBit/s
+ rx bitrate: 48.0 MBit/s`;
+
+ const result = parseIwLinkOutput(iwOutput);
+
+ expect(result.generation).toBe(WIFI_GENERATIONS.WIFI_2);
+ expect(result.standard).toBe('802.11a');
+ });
+ });
+
+ describe('WiFi 1 (802.11b) - 2.4 GHz low bitrate', () => {
+ it('should detect WiFi 1 for 2.4 GHz with bitrate <= 11 Mbps', () => {
+ const iwOutput = `Connected to aa:bb:cc:dd:ee:ff (on wlan0)
+ SSID: VeryOldNetwork
+ freq: 2437
+ signal: -70 dBm
+ tx bitrate: 11.0 MBit/s
+ rx bitrate: 5.5 MBit/s`;
+
+ const result = parseIwLinkOutput(iwOutput);
+
+ expect(result.generation).toBe(WIFI_GENERATIONS.WIFI_1);
+ expect(result.standard).toBe('802.11b');
+ });
+
+ it('should detect WiFi 1 for 2.4 GHz with 1 Mbps bitrate', () => {
+ const iwOutput = `Connected to aa:bb:cc:dd:ee:ff (on wlan0)
+ freq: 2412
+ signal: -80 dBm
+ tx bitrate: 1.0 MBit/s`;
+
+ const result = parseIwLinkOutput(iwOutput);
+
+ expect(result.generation).toBe(WIFI_GENERATIONS.WIFI_1);
+ });
+ });
+
+ describe('WiFi 3 (802.11g) - 2.4 GHz high bitrate', () => {
+ it('should detect WiFi 3 for 2.4 GHz with bitrate > 11 Mbps', () => {
+ const iwOutput = `Connected to aa:bb:cc:dd:ee:ff (on wlan0)
+ SSID: OlderNetwork
+ freq: 2437
+ signal: -55 dBm
+ tx bitrate: 54.0 MBit/s
+ rx bitrate: 36.0 MBit/s`;
+
+ const result = parseIwLinkOutput(iwOutput);
+
+ expect(result.generation).toBe(WIFI_GENERATIONS.WIFI_3);
+ expect(result.standard).toBe('802.11g');
+ });
+
+ it('should detect WiFi 3 for 2.4 GHz with 12 Mbps bitrate', () => {
+ const iwOutput = `Connected to aa:bb:cc:dd:ee:ff (on wlan0)
+ freq: 2462
+ signal: -65 dBm
+ tx bitrate: 12.0 MBit/s`;
+
+ const result = parseIwLinkOutput(iwOutput);
+
+ expect(result.generation).toBe(WIFI_GENERATIONS.WIFI_3);
+ });
+ });
+
+ describe('no legacy detection when generation already known', () => {
+ it('should not override WiFi 4 detection with legacy', () => {
+ const iwOutput = `Connected to aa:bb:cc:dd:ee:ff (on wlan0)
+ freq: 2437
+ signal: -65 dBm
+ tx bitrate: 72.2 MBit/s MCS 7 20MHz short GI`;
+
+ const result = parseIwLinkOutput(iwOutput);
+
+ expect(result.generation).toBe(WIFI_GENERATIONS.WIFI_4);
+ });
+ });
+
+ describe('no legacy detection without enough info', () => {
+ it('should remain UNKNOWN without frequency', () => {
+ const iwOutput = `Connected to aa:bb:cc:dd:ee:ff (on wlan0)
+ signal: -65 dBm
+ tx bitrate: 54.0 MBit/s`;
+
+ const result = parseIwLinkOutput(iwOutput);
+
+ expect(result.generation).toBe(WIFI_GENERATIONS.UNKNOWN);
+ });
+
+ it('should remain UNKNOWN without bitrate on 2.4 GHz', () => {
+ const iwOutput = `Connected to aa:bb:cc:dd:ee:ff (on wlan0)
+ freq: 2437
+ signal: -65 dBm`;
+
+ const result = parseIwLinkOutput(iwOutput);
+
+ expect(result.generation).toBe(WIFI_GENERATIONS.UNKNOWN);
+ });
+ });
+});
+
describe('getGenerationLabel', () => {
it('should return "WiFi X" for known generations', () => {
+ expect(getGenerationLabel(WIFI_GENERATIONS.WIFI_1)).toBe('WiFi 1');
+ expect(getGenerationLabel(WIFI_GENERATIONS.WIFI_2)).toBe('WiFi 2');
+ expect(getGenerationLabel(WIFI_GENERATIONS.WIFI_3)).toBe('WiFi 3');
expect(getGenerationLabel(WIFI_GENERATIONS.WIFI_4)).toBe('WiFi 4');
expect(getGenerationLabel(WIFI_GENERATIONS.WIFI_5)).toBe('WiFi 5');
expect(getGenerationLabel(WIFI_GENERATIONS.WIFI_6)).toBe('WiFi 6');
@@ -241,6 +355,9 @@ describe('getGenerationLabel', () => {
describe('getGenerationDescription', () => {
it('should return full description with IEEE standard', () => {
+ expect(getGenerationDescription(WIFI_GENERATIONS.WIFI_1)).toBe('WiFi 1 (802.11b)');
+ expect(getGenerationDescription(WIFI_GENERATIONS.WIFI_2)).toBe('WiFi 2 (802.11a)');
+ expect(getGenerationDescription(WIFI_GENERATIONS.WIFI_3)).toBe('WiFi 3 (802.11g)');
expect(getGenerationDescription(WIFI_GENERATIONS.WIFI_4)).toBe('WiFi 4 (802.11n)');
expect(getGenerationDescription(WIFI_GENERATIONS.WIFI_5)).toBe('WiFi 5 (802.11ac)');
expect(getGenerationDescription(WIFI_GENERATIONS.WIFI_6)).toBe('WiFi 6 (802.11ax)');
@@ -252,8 +369,30 @@ describe('getGenerationDescription', () => {
});
});
+describe('getGenerationIconFilename', () => {
+ it('should return PNG filename for WiFi 4-7', () => {
+ expect(getGenerationIconFilename(WIFI_GENERATIONS.WIFI_4)).toBe('wifi-4.png');
+ expect(getGenerationIconFilename(WIFI_GENERATIONS.WIFI_5)).toBe('wifi-5.png');
+ expect(getGenerationIconFilename(WIFI_GENERATIONS.WIFI_6)).toBe('wifi-6.png');
+ expect(getGenerationIconFilename(WIFI_GENERATIONS.WIFI_7)).toBe('wifi-7.png');
+ });
+
+ it('should return SVG filename for WiFi 1-3', () => {
+ expect(getGenerationIconFilename(WIFI_GENERATIONS.WIFI_1)).toBe('wifi-1.svg');
+ expect(getGenerationIconFilename(WIFI_GENERATIONS.WIFI_2)).toBe('wifi-2.svg');
+ expect(getGenerationIconFilename(WIFI_GENERATIONS.WIFI_3)).toBe('wifi-3.svg');
+ });
+
+ it('should return null for UNKNOWN', () => {
+ expect(getGenerationIconFilename(WIFI_GENERATIONS.UNKNOWN)).toBeNull();
+ });
+});
+
describe('IEEE_STANDARDS', () => {
it('should map all generations to their IEEE standards', () => {
+ expect(IEEE_STANDARDS[WIFI_GENERATIONS.WIFI_1]).toBe('802.11b');
+ expect(IEEE_STANDARDS[WIFI_GENERATIONS.WIFI_2]).toBe('802.11a');
+ expect(IEEE_STANDARDS[WIFI_GENERATIONS.WIFI_3]).toBe('802.11g');
expect(IEEE_STANDARDS[WIFI_GENERATIONS.WIFI_4]).toBe('802.11n');
expect(IEEE_STANDARDS[WIFI_GENERATIONS.WIFI_5]).toBe('802.11ac');
expect(IEEE_STANDARDS[WIFI_GENERATIONS.WIFI_6]).toBe('802.11ax');
@@ -264,6 +403,9 @@ describe('IEEE_STANDARDS', () => {
describe('GENERATION_CSS_CLASSES', () => {
it('should map all generations to CSS classes', () => {
+ expect(GENERATION_CSS_CLASSES[WIFI_GENERATIONS.WIFI_1]).toBe('wifi-gen-1');
+ expect(GENERATION_CSS_CLASSES[WIFI_GENERATIONS.WIFI_2]).toBe('wifi-gen-2');
+ expect(GENERATION_CSS_CLASSES[WIFI_GENERATIONS.WIFI_3]).toBe('wifi-gen-3');
expect(GENERATION_CSS_CLASSES[WIFI_GENERATIONS.WIFI_4]).toBe('wifi-gen-4');
expect(GENERATION_CSS_CLASSES[WIFI_GENERATIONS.WIFI_5]).toBe('wifi-gen-5');
expect(GENERATION_CSS_CLASSES[WIFI_GENERATIONS.WIFI_6]).toBe('wifi-gen-6');
@@ -272,8 +414,10 @@ describe('GENERATION_CSS_CLASSES', () => {
});
it('should use template literal type pattern', () => {
- // Type check: all values should match the GenerationCssClass type
const classes = Object.values(GENERATION_CSS_CLASSES);
+ expect(classes).toContain('wifi-gen-1');
+ expect(classes).toContain('wifi-gen-2');
+ expect(classes).toContain('wifi-gen-3');
expect(classes).toContain('wifi-gen-4');
expect(classes).toContain('wifi-gen-5');
expect(classes).toContain('wifi-gen-6');
diff --git a/src/wifiGeneration.ts b/src/wifiGeneration.ts
index 7aa08e9..5beed42 100644
--- a/src/wifiGeneration.ts
+++ b/src/wifiGeneration.ts
@@ -81,6 +81,8 @@ export function parseIwLinkOutput(iwOutput: string): IwLinkInfo {
parseLine(line.trim(), result);
}
+ detectLegacyGeneration(result);
+
return freezeResult(result);
}
@@ -101,6 +103,28 @@ function createMutableResult(): MutableParseResult {
};
}
+const WIFI_1_MAX_BITRATE = 11;
+const FREQ_5GHZ_START = 5000;
+
+function detectLegacyGeneration(result: MutableParseResult): void {
+ if (result.generation !== WIFI_GENERATIONS.UNKNOWN) return;
+ if (result.frequency === null) return;
+
+ if (result.frequency >= FREQ_5GHZ_START) {
+ result.generation = WIFI_GENERATIONS.WIFI_2;
+ return;
+ }
+
+ const maxBitrate = Math.max(
+ (result.txBitrate as number | null) ?? 0,
+ (result.rxBitrate as number | null) ?? 0
+ );
+ if (maxBitrate === 0) return;
+
+ result.generation =
+ maxBitrate <= WIFI_1_MAX_BITRATE ? WIFI_GENERATIONS.WIFI_1 : WIFI_GENERATIONS.WIFI_3;
+}
+
function freezeResult(result: MutableParseResult): IwLinkInfo {
if (isKnownGeneration(result.generation)) {
result.standard = IEEE_STANDARDS[result.generation];
@@ -285,3 +309,18 @@ export function getGenerationDescription(generation: WifiGeneration): string {
? `WiFi ${generation} (${IEEE_STANDARDS[generation]})`
: 'WiFi';
}
+
+const GENERATION_ICON_FILENAMES: Record = {
+ [WIFI_GENERATIONS.WIFI_1]: 'wifi-1.svg',
+ [WIFI_GENERATIONS.WIFI_2]: 'wifi-2.svg',
+ [WIFI_GENERATIONS.WIFI_3]: 'wifi-3.svg',
+ [WIFI_GENERATIONS.WIFI_4]: 'wifi-4.png',
+ [WIFI_GENERATIONS.WIFI_5]: 'wifi-5.png',
+ [WIFI_GENERATIONS.WIFI_6]: 'wifi-6.png',
+ [WIFI_GENERATIONS.WIFI_7]: 'wifi-7.png',
+ [WIFI_GENERATIONS.UNKNOWN]: null,
+} as const;
+
+export function getGenerationIconFilename(generation: WifiGeneration): string | null {
+ return GENERATION_ICON_FILENAMES[generation];
+}