1717
1818import time
1919from collections .abc import Callable
20+ from dataclasses import replace
2021from pathlib import Path
2122from typing import TYPE_CHECKING
2223
3334from docking .applets .network .render import create_icon
3435from docking .applets .network .state import (
3536 AvailableNetwork ,
37+ NetworkState ,
3638 TrafficCounters ,
3739 build_tooltip ,
3840 compute_speeds ,
@@ -83,15 +85,8 @@ def __init__(self, icon_size: int, config: Config | None = None) -> None:
8385 self ._nm_handler_id : int = 0
8486 self ._nm_state_handler_id : int = 0
8587
86- # State
87- self ._is_connected = False
88- self ._is_wifi = False
89- self ._ssid = ""
90- self ._signal_strength = 0
91- self ._iface = ""
92- self ._ip_address = ""
93- self ._rx_speed = 0.0
94- self ._tx_speed = 0.0
88+ # Visible state. Frozen so equality gates re-renders.
89+ self ._state = NetworkState ()
9590 # "download", "upload", or "none"
9691 self ._speed_overlay = "download"
9792
@@ -111,11 +106,11 @@ def create_icon(self, size: int):
111106 """Load network icon with optional speed overlay."""
112107 return create_icon (
113108 size = size ,
114- is_connected = self ._is_connected ,
115- is_wifi = self ._is_wifi ,
116- signal_strength = self ._signal_strength ,
117- rx_speed = self ._rx_speed ,
118- tx_speed = self ._tx_speed ,
109+ is_connected = self ._state . is_connected ,
110+ is_wifi = self ._state . is_wifi ,
111+ signal_strength = self ._state . signal_strength ,
112+ rx_speed = self ._state . rx_speed ,
113+ tx_speed = self ._state . tx_speed ,
119114 speed_overlay = self ._speed_overlay ,
120115 )
121116
@@ -124,34 +119,35 @@ def refresh_tooltip(self) -> None:
124119
125120 def get_menu_items (self ) -> list :
126121 """Show connection info and common network actions."""
122+ state = self ._state
127123 status : list [Gtk .MenuItem ] = []
128- if self . _ssid :
124+ if state . ssid :
129125 status .append (
130126 disabled_menu_item (
131127 _ ("WiFi: {ssid} ({pct}%)" ).format (
132- ssid = self . _ssid , pct = self . _signal_strength
128+ ssid = state . ssid , pct = state . signal_strength
133129 ),
134130 gtk = Gtk ,
135131 )
136132 )
137- elif self . _is_connected :
133+ elif state . is_connected :
138134 status .append (
139135 disabled_menu_item (
140- _ ("Ethernet: {iface}" ).format (iface = self . _iface ),
136+ _ ("Ethernet: {iface}" ).format (iface = state . iface ),
141137 gtk = Gtk ,
142138 )
143139 )
144140 else :
145141 status .append (disabled_menu_item (_ ("Not connected" ), gtk = Gtk ))
146142
147- if self . _ip_address :
143+ if state . ip_address :
148144 status .append (
149- disabled_menu_item (_ ("IP: {ip}" ).format (ip = self . _ip_address ), gtk = Gtk )
145+ disabled_menu_item (_ ("IP: {ip}" ).format (ip = state . ip_address ), gtk = Gtk )
150146 )
151147
152- if self . _is_connected :
153- down = format_speed (bps = self . _rx_speed )
154- up = format_speed (bps = self . _tx_speed )
148+ if state . is_connected :
149+ down = format_speed (bps = state . rx_speed )
150+ up = format_speed (bps = state . tx_speed )
155151 status .append (disabled_menu_item (f"\u2193 { down } \u2191 { up } " , gtk = Gtk ))
156152
157153 settings : list [Gtk .MenuItem ] = []
@@ -246,8 +242,7 @@ def _set_networking_enabled(self, *, enabled: bool) -> None:
246242 exc ,
247243 )
248244 return
249- self ._update_nm_state ()
250- self .present ()
245+ self ._refresh_state ()
251246
252247 def _set_wireless_enabled (self , * , enabled : bool ) -> None :
253248 if self ._nm_client is None :
@@ -261,8 +256,7 @@ def _set_wireless_enabled(self, *, enabled: bool) -> None:
261256 exc ,
262257 )
263258 return
264- self ._update_nm_state ()
265- self .present ()
259+ self ._refresh_state ()
266260
267261 def _build_available_networks_submenu (self ) -> Gtk .MenuItem :
268262 item = Gtk .MenuItem (label = _ ("Available Networks" ))
@@ -506,7 +500,7 @@ def start(self, notify: Callable[[], None]) -> None:
506500 "notify::state" ,
507501 self ._on_nm_changed ,
508502 )
509- self ._update_nm_state ()
503+ self ._refresh_state ()
510504 except GLib .Error :
511505 log .bind (action = "connect_nm" ).warning (
512506 "Could not connect to NetworkManager" ,
@@ -529,21 +523,21 @@ def stop(self) -> None:
529523 super ().stop ()
530524
531525 def _on_nm_changed (self , * _args : object ) -> None :
532- """NM active-connections changed: update state immediately."""
533- self ._update_nm_state ()
534- self .present ()
535-
536- def _update_nm_state (self ) -> None :
537- """Read current connection info from NetworkManager."""
526+ """NM active-connections changed: refresh and re-render on change."""
527+ self ._refresh_state ()
528+
529+ def _refresh_state (self ) -> None :
530+ """Recompute the full visible state and present only on change."""
531+ new_state = self ._compute_nm_state ()
532+ new_state = self ._apply_traffic (state = new_state )
533+ if new_state != self ._state :
534+ self ._state = new_state
535+ self .present ()
536+
537+ def _compute_nm_state (self ) -> NetworkState :
538+ """Return a NetworkState reflecting current NetworkManager status."""
538539 if not self ._nm_client :
539- return
540-
541- self ._is_connected = False
542- self ._is_wifi = False
543- self ._ssid = ""
544- self ._signal_strength = 0
545- self ._iface = ""
546- self ._ip_address = ""
540+ return NetworkState ()
547541
548542 # Collect candidates, prioritize wifi > ethernet > other
549543 best_device : NM .Device | None = None
@@ -564,85 +558,109 @@ def _update_nm_state(self) -> None:
564558 best_device = device
565559
566560 if not best_device :
567- return
561+ return NetworkState ()
568562
569- self ._is_connected = True
570- self ._iface = best_device .get_iface () or ""
571-
572- # IP address
563+ iface = best_device .get_iface () or ""
564+ ip_address = ""
573565 ip4_config = best_device .get_ip4_config ()
574566 if ip4_config :
575567 addrs = ip4_config .get_addresses ()
576568 if addrs :
577- self . _ip_address = addrs [0 ].get_address () or ""
569+ ip_address = addrs [0 ].get_address () or ""
578570
579- # WiFi specifics
571+ is_wifi = False
572+ ssid = ""
573+ signal_strength = 0
580574 if isinstance (best_device , NM .DeviceWifi ):
581- self . _is_wifi = True
575+ is_wifi = True
582576 ap = best_device .get_active_access_point ()
583577 if ap :
584578 ssid_bytes = ap .get_ssid ()
585579 if ssid_bytes :
586- self ._ssid = ssid_bytes .get_data ().decode ("utf-8" , errors = "replace" )
587- self ._signal_strength = ap .get_strength ()
580+ ssid = ssid_bytes .get_data ().decode ("utf-8" , errors = "replace" )
581+ signal_strength = ap .get_strength ()
582+
583+ return NetworkState (
584+ is_connected = True ,
585+ is_wifi = is_wifi ,
586+ ssid = ssid ,
587+ signal_strength = signal_strength ,
588+ iface = iface ,
589+ ip_address = ip_address ,
590+ # Traffic speeds get filled in by _apply_traffic; keep previous values
591+ # so equality comparison doesn't flap between NM refresh and tick.
592+ rx_speed = self ._state .rx_speed ,
593+ tx_speed = self ._state .tx_speed ,
594+ )
588595
589596 def _tick (self ) -> bool :
590- """Poll traffic counters and wifi signal."""
591- self ._update_nm_state ()
592- self ._update_traffic ()
593- self ._update_wifi_signal ()
594- self .present ()
597+ """Poll traffic counters and wifi signal, re-render only on change."""
598+ self ._refresh_state ()
595599 return True
596600
597- def _update_traffic (self ) -> None :
598- """Read /proc/net/dev and compute speeds for active interface."""
599- if not self ._iface :
600- self ._rx_speed = 0.0
601- self ._tx_speed = 0.0
602- return
601+ def _apply_traffic (self , * , state : NetworkState ) -> NetworkState :
602+ """Return ``state`` with rx/tx speeds updated from /proc/net/dev."""
603+ if not state .iface :
604+ self ._prev_counters = None
605+ return replace (state , rx_speed = 0.0 , tx_speed = 0.0 )
603606 try :
604607 with _PROC_NET_DEV .open () as f :
605608 counters = parse_proc_net_dev (text = f .read ())
606609 except OSError as exc :
607610 log .debug ("Failed to read %s: %s" , _PROC_NET_DEV , exc )
608- return
611+ return state
609612
610613 now = time .monotonic ()
611- current = counters .get (self ._iface )
614+ current = counters .get (state .iface )
615+ rx_speed = state .rx_speed
616+ tx_speed = state .tx_speed
612617 if current and self ._prev_counters :
613618 elapsed = now - self ._prev_time
614- self . _rx_speed , self . _tx_speed = compute_speeds (
619+ rx_speed , tx_speed = compute_speeds (
615620 prev = self ._prev_counters ,
616621 curr = current ,
617622 elapsed_s = elapsed ,
618623 )
619624 if current :
620625 self ._prev_counters = current
621626 self ._prev_time = now
627+ # NM may have stale signal strength; refresh while we have the client.
628+ signal = self ._refresh_wifi_signal (state = state )
629+ return NetworkState (
630+ is_connected = state .is_connected ,
631+ is_wifi = state .is_wifi ,
632+ ssid = state .ssid ,
633+ signal_strength = signal ,
634+ iface = state .iface ,
635+ ip_address = state .ip_address ,
636+ rx_speed = rx_speed ,
637+ tx_speed = tx_speed ,
638+ )
622639
623- def _update_wifi_signal (self ) -> None :
624- """Re-read wifi signal from NM (access point strength can change) ."""
625- if not self ._nm_client or not self . _is_wifi :
626- return
640+ def _refresh_wifi_signal (self , * , state : NetworkState ) -> int :
641+ """Return the current wifi signal strength, or ``state.signal_strength`` ."""
642+ if not self ._nm_client or not state . is_wifi :
643+ return state . signal_strength
627644 for conn in self ._nm_client .get_active_connections ():
628645 if conn .get_state () != NM .ActiveConnectionState .ACTIVATED :
629646 continue
630647 for device in conn .get_devices () or ():
631648 if isinstance (device , NM .DeviceWifi ):
632649 ap = device .get_active_access_point ()
633650 if ap :
634- self ._signal_strength = ap .get_strength ()
635- return
651+ return ap .get_strength ()
652+ return state .signal_strength
653+ return state .signal_strength
636654
637655 def _build_tooltip (self ) -> str :
638656 return build_tooltip (
639- is_connected = self ._is_connected ,
640- ssid = self ._ssid ,
641- signal_strength = self ._signal_strength ,
642- iface = self ._iface ,
643- ip_address = self ._ip_address ,
644- rx_speed = self ._rx_speed ,
645- tx_speed = self ._tx_speed ,
657+ is_connected = self ._state . is_connected ,
658+ ssid = self ._state . ssid ,
659+ signal_strength = self ._state . signal_strength ,
660+ iface = self ._state . iface ,
661+ ip_address = self ._state . ip_address ,
662+ rx_speed = self ._state . rx_speed ,
663+ tx_speed = self ._state . tx_speed ,
646664 )
647665
648666 def _has_wifi_device (self ) -> bool :
0 commit comments