Refonte du popup : layout deux colonnes, jauges de performance et graphique signal
- Remplace les PopupMenuItem textuels par des PopupBaseMenuItem avec labels à gauche (dimmed) et valeurs alignées à droite - Ajoute des headers de section (Performance, Signal) via PopupSeparatorMenuItem - Ajoute des jauges horizontales pour Speed et Width avec layout vertical (label+valeur au-dessus, barre pleine largeur en dessous) - Ajoute un graphique d'historique du signal (St.DrawingArea + Cairo) coloré selon la qualité (60 points, 5 min d'historique) - Rafraîchit les données à l'ouverture du popup pour un affichage immédiat - Max speed basé sur le théorique WiFi 7 4×4 MIMO 320 MHz (5760 Mbit/s) - Max width basé sur le WiFi 7 (320 MHz)
This commit is contained in:
parent
b9c51fd010
commit
a5ef20d4f1
2 changed files with 243 additions and 28 deletions
240
src/extension.ts
240
src/extension.ts
|
|
@ -31,6 +31,20 @@ import type { GenerationCssClass, ChannelWidthMHz, SignalDbm } from './types.js'
|
|||
|
||||
const REFRESH_INTERVAL_SECONDS = 5;
|
||||
const PLACEHOLDER = '--' as const;
|
||||
// WiFi 7 theoretical max: 320 MHz, MCS 13 (4096-QAM 5/6), 4×4 MIMO, GI 0.8µs
|
||||
const MAX_SPEED_MBPS = 5760;
|
||||
const MAX_CHANNEL_WIDTH_MHZ = 320;
|
||||
const MIN_SIGNAL_DBM = -90;
|
||||
const MAX_SIGNAL_DBM = -30;
|
||||
const SIGNAL_HISTORY_MAX = 60;
|
||||
|
||||
const SIGNAL_QUALITY_COLORS: Readonly<Record<string, [number, number, number]>> = {
|
||||
Excellent: [0.2, 0.82, 0.48],
|
||||
Good: [0.56, 0.94, 0.64],
|
||||
Fair: [0.96, 0.83, 0.18],
|
||||
Weak: [1.0, 0.47, 0.0],
|
||||
Poor: [0.88, 0.11, 0.14],
|
||||
};
|
||||
|
||||
type MenuItemId =
|
||||
| 'ssid'
|
||||
|
|
@ -69,13 +83,20 @@ const MENU_STRUCTURE: readonly MenuItemConfig[][] = [
|
|||
],
|
||||
] as const;
|
||||
|
||||
const ITEMS_WITH_BAR: ReadonlySet<MenuItemId> = new Set(['bitrate', 'channelWidth']);
|
||||
|
||||
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;
|
||||
private readonly menuItems = new Map<MenuItemId, PopupMenu.PopupMenuItem>();
|
||||
private signalGraph: St.DrawingArea | null = null;
|
||||
private readonly signalHistory: number[] = [];
|
||||
private readonly menuItems = new Map<
|
||||
MenuItemId,
|
||||
{ item: PopupMenu.PopupBaseMenuItem; label: St.Label; value: St.Label; barFill?: St.Widget }
|
||||
>();
|
||||
|
||||
enable(): void {
|
||||
this.wifiService = new WifiInfoService();
|
||||
|
|
@ -100,6 +121,8 @@ export default class WifiSignalPlusExtension extends Extension {
|
|||
this.wifiService = null;
|
||||
this.icon = null;
|
||||
this.label = null;
|
||||
this.signalGraph = null;
|
||||
this.signalHistory.length = 0;
|
||||
this.menuItems.clear();
|
||||
}
|
||||
|
||||
|
|
@ -130,28 +153,164 @@ export default class WifiSignalPlusExtension extends Extension {
|
|||
|
||||
const menu = this.indicator.menu as PopupMenu.PopupMenu;
|
||||
menu.box.add_style_class_name('wifi-signal-plus-popup');
|
||||
menu.connect('open-state-changed', (_menu, isOpen: boolean) => {
|
||||
if (isOpen) this.refresh();
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const sectionHeaders = ['', 'Performance', 'Signal'];
|
||||
|
||||
const SIGNAL_SECTION_INDEX = 2;
|
||||
|
||||
MENU_STRUCTURE.forEach((section, index) => {
|
||||
for (const { id, label } of section) {
|
||||
this.addMenuItem(menu, id, label);
|
||||
if (sectionHeaders[index]) {
|
||||
menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem(sectionHeaders[index]));
|
||||
}
|
||||
|
||||
// Add separator between sections (not after last)
|
||||
if (index < MENU_STRUCTURE.length - 1) {
|
||||
menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
||||
if (index === SIGNAL_SECTION_INDEX) {
|
||||
this.addSignalGraph(menu);
|
||||
}
|
||||
|
||||
for (const { id, label } of section) {
|
||||
this.addMenuItem(menu, id, label, ITEMS_WITH_BAR.has(id));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addMenuItem(menu: PopupMenu.PopupMenu, id: MenuItemId, label: string): void {
|
||||
const item = new PopupMenu.PopupMenuItem(`${label}: ${PLACEHOLDER}`, { reactive: false });
|
||||
menu.addMenuItem(item);
|
||||
this.menuItems.set(id, item);
|
||||
private addMenuItem(
|
||||
menu: PopupMenu.PopupMenu,
|
||||
id: MenuItemId,
|
||||
label: string,
|
||||
withBar = false,
|
||||
): void {
|
||||
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
|
||||
item.add_style_class_name('wifi-popup-item');
|
||||
|
||||
const labelWidget = new St.Label({
|
||||
text: label,
|
||||
style_class: 'wifi-popup-label',
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
});
|
||||
|
||||
const valueWidget = new St.Label({
|
||||
text: PLACEHOLDER,
|
||||
style_class: 'wifi-popup-value',
|
||||
x_expand: true,
|
||||
x_align: Clutter.ActorAlign.END,
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
});
|
||||
|
||||
let barFill: St.Widget | undefined;
|
||||
if (withBar) {
|
||||
const box = new St.BoxLayout({ vertical: true, x_expand: true });
|
||||
|
||||
const row = new St.BoxLayout({ x_expand: true });
|
||||
row.add_child(labelWidget);
|
||||
row.add_child(valueWidget);
|
||||
box.add_child(row);
|
||||
|
||||
const barTrack = new St.Widget({
|
||||
style_class: 'wifi-bar-track',
|
||||
x_expand: true,
|
||||
});
|
||||
barFill = new St.Widget({ style_class: 'wifi-bar-fill' });
|
||||
barTrack.add_child(barFill);
|
||||
box.add_child(barTrack);
|
||||
|
||||
item.add_child(box);
|
||||
} else {
|
||||
item.add_child(labelWidget);
|
||||
item.add_child(valueWidget);
|
||||
}
|
||||
|
||||
private updateMenuItem(id: MenuItemId, label: string, value: string): void {
|
||||
const item = this.menuItems.get(id);
|
||||
item?.label.set_text(`${label}: ${value}`);
|
||||
menu.addMenuItem(item);
|
||||
this.menuItems.set(id, { item, label: labelWidget, value: valueWidget, barFill });
|
||||
}
|
||||
|
||||
private addSignalGraph(menu: PopupMenu.PopupMenu): void {
|
||||
const item = new PopupMenu.PopupBaseMenuItem({ reactive: false });
|
||||
item.add_style_class_name('wifi-popup-item');
|
||||
|
||||
this.signalGraph = new St.DrawingArea({
|
||||
style_class: 'wifi-signal-graph',
|
||||
x_expand: true,
|
||||
});
|
||||
this.signalGraph.connect('repaint', () => this.drawSignalGraph());
|
||||
|
||||
item.add_child(this.signalGraph);
|
||||
menu.addMenuItem(item);
|
||||
}
|
||||
|
||||
private drawSignalGraph(): void {
|
||||
if (!this.signalGraph) return;
|
||||
|
||||
const cr = this.signalGraph.get_context();
|
||||
const [width, height] = this.signalGraph.get_surface_size();
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
cr.$dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Background
|
||||
cr.setSourceRGBA(1, 1, 1, 0.05);
|
||||
cr.rectangle(0, 0, width, height);
|
||||
cr.fill();
|
||||
|
||||
if (this.signalHistory.length < 2) {
|
||||
cr.$dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const mapY = (dbm: number): number => {
|
||||
const normalized = Math.max(
|
||||
0,
|
||||
Math.min(1, (dbm - MIN_SIGNAL_DBM) / (MAX_SIGNAL_DBM - MIN_SIGNAL_DBM)),
|
||||
);
|
||||
return height * (1 - normalized);
|
||||
};
|
||||
|
||||
const stepX = width / (SIGNAL_HISTORY_MAX - 1);
|
||||
const startX = width - (this.signalHistory.length - 1) * stepX;
|
||||
|
||||
const latest = this.signalHistory[this.signalHistory.length - 1];
|
||||
const quality = getSignalQuality(latest as SignalDbm);
|
||||
const [r, g, b] = SIGNAL_QUALITY_COLORS[quality] ?? [0.2, 0.52, 0.89];
|
||||
|
||||
// Filled area
|
||||
cr.moveTo(startX, height);
|
||||
for (let i = 0; i < this.signalHistory.length; i++) {
|
||||
cr.lineTo(startX + i * stepX, mapY(this.signalHistory[i]));
|
||||
}
|
||||
cr.lineTo(startX + (this.signalHistory.length - 1) * stepX, height);
|
||||
cr.closePath();
|
||||
cr.setSourceRGBA(r, g, b, 0.2);
|
||||
cr.fill();
|
||||
|
||||
// Line on top
|
||||
cr.moveTo(startX, mapY(this.signalHistory[0]));
|
||||
for (let i = 1; i < this.signalHistory.length; i++) {
|
||||
cr.lineTo(startX + i * stepX, mapY(this.signalHistory[i]));
|
||||
}
|
||||
cr.setSourceRGBA(r, g, b, 0.8);
|
||||
cr.setLineWidth(1.5);
|
||||
cr.stroke();
|
||||
|
||||
cr.$dispose();
|
||||
}
|
||||
|
||||
private updateMenuItem(id: MenuItemId, value: string, barPercent?: number): void {
|
||||
const entry = this.menuItems.get(id);
|
||||
if (!entry) return;
|
||||
|
||||
entry.value.set_text(value);
|
||||
|
||||
if (entry.barFill !== undefined && barPercent !== undefined) {
|
||||
const trackWidth = entry.barFill.get_parent()?.width ?? 0;
|
||||
if (trackWidth > 0) {
|
||||
entry.barFill.set_width(Math.round((barPercent / 100) * trackWidth));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
|
|
@ -211,23 +370,54 @@ export default class WifiSignalPlusExtension extends Extension {
|
|||
|
||||
private showDisconnectedState(): void {
|
||||
for (const section of MENU_STRUCTURE) {
|
||||
for (const { id, label } of section) {
|
||||
for (const { id } of section) {
|
||||
const value = id === 'ssid' ? 'Not connected' : PLACEHOLDER;
|
||||
this.updateMenuItem(id, label, value);
|
||||
}
|
||||
this.updateMenuItem(id, value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.signalHistory.length = 0;
|
||||
this.signalGraph?.queue_repaint();
|
||||
}
|
||||
|
||||
private showConnectedState(info: ConnectedInfo): void {
|
||||
this.updateMenuItem('ssid', 'Network', info.ssid);
|
||||
this.updateMenuItem('generation', 'Generation', getGenerationDescription(info.generation));
|
||||
this.updateMenuItem('band', 'Band', this.formatBand(info));
|
||||
this.updateMenuItem('bitrate', 'Speed', this.formatBitrate(info));
|
||||
this.updateMenuItem('channelWidth', 'Width', this.formatChannelWidth(info.channelWidth));
|
||||
this.updateMenuItem('mcs', 'Modulation', this.formatModulation(info));
|
||||
this.updateMenuItem('signal', 'Signal', this.formatSignal(info.signalStrength));
|
||||
this.updateMenuItem('security', 'Security', info.security);
|
||||
this.updateMenuItem('bssid', 'BSSID', info.bssid);
|
||||
this.updateMenuItem('ssid', info.ssid);
|
||||
this.updateMenuItem('generation', getGenerationDescription(info.generation));
|
||||
this.updateMenuItem('band', this.formatBand(info));
|
||||
this.updateMenuItem(
|
||||
'bitrate',
|
||||
this.formatBitrate(info),
|
||||
this.getSpeedPercent(info),
|
||||
);
|
||||
this.updateMenuItem(
|
||||
'channelWidth',
|
||||
this.formatChannelWidth(info.channelWidth),
|
||||
this.getWidthPercent(info.channelWidth),
|
||||
);
|
||||
this.updateMenuItem('mcs', this.formatModulation(info));
|
||||
this.updateMenuItem('signal', this.formatSignal(info.signalStrength));
|
||||
this.updateMenuItem('security', info.security);
|
||||
this.updateMenuItem('bssid', info.bssid);
|
||||
|
||||
this.pushSignalHistory(info.signalStrength);
|
||||
}
|
||||
|
||||
private pushSignalHistory(signalStrength: SignalDbm): void {
|
||||
this.signalHistory.push(signalStrength as number);
|
||||
if (this.signalHistory.length > SIGNAL_HISTORY_MAX) {
|
||||
this.signalHistory.shift();
|
||||
}
|
||||
this.signalGraph?.queue_repaint();
|
||||
}
|
||||
|
||||
private getSpeedPercent(info: ConnectedInfo): number {
|
||||
const speed = Math.max(info.txBitrate ?? 0, info.rxBitrate ?? 0, info.bitrate);
|
||||
return Math.min(100, (speed / MAX_SPEED_MBPS) * 100);
|
||||
}
|
||||
|
||||
private getWidthPercent(width: ChannelWidthMHz | null): number {
|
||||
if (width === null) return 0;
|
||||
return Math.min(100, (width / MAX_CHANNEL_WIDTH_MHZ) * 100);
|
||||
}
|
||||
|
||||
private formatBand(info: ConnectedInfo): string {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
/* Tooltip popup */
|
||||
.wifi-signal-plus-popup {
|
||||
padding: 12px;
|
||||
min-width: 280px;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.wifi-popup-section {
|
||||
|
|
@ -60,17 +60,42 @@
|
|||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.wifi-popup-row {
|
||||
padding: 2px 0;
|
||||
.wifi-popup-item {
|
||||
min-height: 0;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.wifi-popup-label {
|
||||
opacity: 0.7;
|
||||
margin-right: 8px;
|
||||
font-size: 1em;
|
||||
min-width: 7em;
|
||||
padding: 0 0.5em;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.wifi-popup-value {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
/* Signal history graph */
|
||||
.wifi-signal-graph {
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Bar gauges */
|
||||
.wifi-bar-track {
|
||||
height: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.wifi-bar-fill {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background-color: #3584e4;
|
||||
}
|
||||
|
||||
/* Signal strength indicator */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue