from __future__ import annotations from dataclasses import dataclass from .model import ( Connectivity, ErrorCode, EthernetLink, LinkDetails, LinkState, NetworkError, NetworkSnapshot, Security, WifiNetwork, list_order, signal_icon, signal_quality, ) ERROR_TEXT = { ErrorCode.PASSWORD_REQUIRED: "This network needs a password.", ErrorCode.WRONG_PASSWORD: "Wrong password. Check it and try again.", ErrorCode.UNSUPPORTED_SECURITY: "This security type isn't supported yet.", ErrorCode.TIMEOUT: "The network didn't respond in time.", ErrorCode.BUSY: "Wi-Fi is busy, try again in a moment.", ErrorCode.NOT_FOUND: "That network is no longer in range.", ErrorCode.CANCELLED: "Connection cancelled.", ErrorCode.UNAVAILABLE: "Wi-Fi isn't available right now.", ErrorCode.FAILED: "Couldn't complete that. Please try again.", } CONNECTIVITY_TEXT = { Connectivity.UNKNOWN: "", Connectivity.LOCAL_ONLY: "Local network only", Connectivity.LIMITED: "No internet", Connectivity.PORTAL: "Sign-in required", Connectivity.FULL: "Internet access", } SECTION_TITLES = { "current": "Current", "saved": "Saved networks", "available": "Available networks", } def error_message(error: Exception) -> str: if isinstance(error, NetworkError): return ERROR_TEXT.get(error.code, ERROR_TEXT[ErrorCode.FAILED]) return ERROR_TEXT[ErrorCode.FAILED] def should_ignore(error: Exception) -> bool: return isinstance(error, NetworkError) and error.code is ErrorCode.CANCELLED def needs_password_prompt(network: WifiNetwork) -> bool: return ( not network.saved and network.security is Security.PSK and network.state is LinkState.IDLE ) def passphrase_problem(text: str) -> str | None: if not text: return "Enter the network password." if len(text) < 8: return "WPA passwords have at least 8 characters." if len(text) > 63: return "WPA passwords have at most 63 characters." return None def is_supported(network: WifiNetwork) -> bool: return network.saved or network.security in (Security.OPEN, Security.PSK) def format_mbps(value: float | None) -> str | None: if value is None: return None if value >= 1000: return f"{value / 1000:.1f} Gbps" return f"{value:.0f} Mbps" def format_speed(speed_mbps: int | None) -> str | None: return format_mbps(float(speed_mbps)) if speed_mbps else None def connectivity_text(connectivity: Connectivity) -> str: return CONNECTIVITY_TEXT[connectivity] def network_icon(network: WifiNetwork) -> str: return signal_icon(network.strength) def row_subtitle(network: WifiNetwork, show_dbm: bool = False) -> str: parts = [] if network.saved: parts.append("Saved") parts.append(network.security.label) parts.append(signal_quality(network.strength)) if show_dbm and network.signal_dbm is not None: parts.append(f"{network.signal_dbm:.0f} dBm") return " · ".join(parts) def row_status(network: WifiNetwork) -> str: return { LinkState.CONNECTED: "Connected", LinkState.CONNECTING: "Connecting…", LinkState.DISCONNECTING: "Disconnecting…", LinkState.IDLE: "", }[network.state] def matches(network: WifiNetwork, query: str) -> bool: query = query.strip().casefold() return not query or query in network.ssid.casefold() def sections( networks: tuple[WifiNetwork, ...], query: str = "" ) -> list[tuple[str, str, list[WifiNetwork]]]: groups: dict[str, list[WifiNetwork]] = {"current": [], "saved": [], "available": []} for network in sorted((n for n in networks if matches(n, query)), key=list_order): if network.is_connected or network.is_busy: groups["current"].append(network) elif network.saved: groups["saved"].append(network) else: groups["available"].append(network) return [(key, SECTION_TITLES[key], members) for key, members in groups.items() if members] @dataclass(frozen=True) class Headline: icon: str title: str subtitle: str tone: str def _connected_ethernet(snapshot: NetworkSnapshot) -> EthernetLink | None: return next((link for link in snapshot.ethernet if link.connected), None) def _wifi_subtitle(network: WifiNetwork, snapshot: NetworkSnapshot) -> str: parts = [signal_quality(network.strength)] if snapshot.wifi.link and snapshot.wifi.link.band: parts.append(snapshot.wifi.link.band) internet = connectivity_text(snapshot.connectivity) if internet: parts.append(internet) return " · ".join(parts) def _tone_for(connectivity: Connectivity) -> str: if connectivity in (Connectivity.LIMITED, Connectivity.LOCAL_ONLY, Connectivity.PORTAL): return "warn" return "ok" def headline(snapshot: NetworkSnapshot) -> Headline: wired = _connected_ethernet(snapshot) wifi = snapshot.wifi active = snapshot.active_network pending = snapshot.pending_network if wired is not None: parts = [format_speed(wired.speed_mbps), connectivity_text(snapshot.connectivity)] return Headline( "settings_ethernet", "Ethernet", " · ".join(p for p in parts if p) or wired.interface, _tone_for(snapshot.connectivity), ) if pending is not None: verb = "Disconnecting from" if pending.state is LinkState.DISCONNECTING else "Connecting to" return Headline("wifi_find", pending.ssid, f"{verb} network…", "busy") if active is not None: return Headline( network_icon(active), active.ssid, _wifi_subtitle(active, snapshot), _tone_for(snapshot.connectivity), ) if not wifi.available: return Headline("signal_wifi_off", "No Wi-Fi adapter", "Plug in or enable a wireless device", "idle") if not wifi.enabled: return Headline("signal_wifi_off", "Wi-Fi is off", "Turn it on to see nearby networks", "idle") if wifi.scanning: return Headline("wifi_find", "Searching…", "Looking for networks nearby", "busy") return Headline("signal_wifi_statusbar_not_connected", "Not connected", "Pick a network below", "idle") def link_facts(link: LinkDetails | None) -> list[tuple[str, str]]: if link is None: return [] facts = [] if link.band: facts.append(("router", link.band)) if link.rx_mbps is not None: facts.append(("arrow_downward", format_mbps(link.rx_mbps))) if link.tx_mbps is not None: facts.append(("arrow_upward", format_mbps(link.tx_mbps))) if link.max_mbps is not None and link.rx_mbps is None: facts.append(("speed", f"up to {format_mbps(link.max_mbps)}")) if link.bssid: facts.append(("fingerprint", link.bssid.upper())) return facts def ethernet_title(link: EthernetLink) -> str: return link.interface def ethernet_subtitle(link: EthernetLink) -> str: if not link.connected: return "Cable unplugged" parts = [format_speed(link.speed_mbps), link.mac.upper() if link.mac else None] return " · ".join(p for p in parts if p) or "Connected" @dataclass(frozen=True) class BarState: icon: str | None tooltip: str def bar_state(snapshot: NetworkSnapshot) -> BarState: wired = _connected_ethernet(snapshot) wifi = snapshot.wifi summary = headline(snapshot) tooltip = summary.title if not summary.subtitle else f"{summary.title}\n{summary.subtitle}" if wired is None and not wifi.available: return BarState(None, tooltip) degraded = snapshot.connectivity in (Connectivity.LIMITED, Connectivity.LOCAL_ONLY) portal = snapshot.connectivity is Connectivity.PORTAL if wired is not None: if portal: return BarState("captive_portal", tooltip) return BarState("link_off" if degraded else "settings_ethernet", tooltip) if snapshot.pending_network is not None: return BarState("wifi_find", tooltip) active = snapshot.active_network if active is not None: if portal: return BarState("captive_portal", tooltip) if degraded: return BarState("signal_wifi_bad", tooltip) return BarState(network_icon(active), tooltip) return BarState(summary.icon, tooltip)