Skip to content

Commit da76dec

Browse files
committed
fix(network): persist ethernet off in the wired profile, move numpad dot left of zero
1 parent a0df3f3 commit da76dec

3 files changed

Lines changed: 114 additions & 16 deletions

File tree

BlocksScreen/lib/network/worker.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -437,24 +437,38 @@ async def _detect_interfaces(self) -> None:
437437
if not self._primary_wifi_path:
438438
logger.warning("No Wi-Fi interface detected; ethernet-only mode")
439439

440+
async def _set_wired_profiles_autoconnect(self, enabled: bool) -> None:
441+
"""Persist autoconnect on every wired profile; Device.Autoconnect dies on NM restart."""
442+
try:
443+
paths = await self._nm_settings().list_connections()
444+
for path, settings in await self._gather_settings(list(paths)):
445+
conn = settings.get("connection", {})
446+
if conn.get("type", (None, ""))[1] != "802-3-ethernet":
447+
continue
448+
if bool(conn.get("autoconnect", ("b", True))[1]) == enabled:
449+
continue
450+
props = {k: dict(v) for k, v in settings.items()}
451+
props["connection"]["autoconnect"] = ("b", enabled)
452+
props["connection"].pop("timestamp", None)
453+
await self._conn_settings(path).update(props)
454+
logger.info("Wired profile %s autoconnect -> %s", path, enabled)
455+
except Exception as exc:
456+
logger.warning("Wired profile autoconnect (%s) failed: %s", enabled, exc)
457+
440458
async def _ensure_wired_autoconnect(self) -> None:
441-
"""Re-arm wired autoconnect; NM's Disconnect() latches it off for good.
459+
"""Re-arm wired autoconnect on both the device and the saved profiles.
442460
443-
Called only when the user asks for ethernet, so the latch keeps meaning
444-
"user turned it off" everywhere else. Best-effort: never propagates.
461+
Called only when the user asks for ethernet, so autoconnect staying off
462+
keeps meaning "user turned it off". Best-effort: never propagates.
445463
"""
446464
if not self._primary_wired_path:
447465
return
466+
await self._set_wired_profiles_autoconnect(True)
448467
try:
449468
wired = self._generic(self._primary_wired_path)
450-
state = await wired.state
451-
auto = bool(await wired.autoconnect)
452-
logger.debug(
453-
"wired autoconnect check: state=%s autoconnect=%s", state, auto
454-
)
455-
if not auto:
469+
if not await wired.autoconnect:
456470
await wired.autoconnect.set_async(True)
457-
logger.info("Re-armed wired autoconnect (was off, state=%s)", state)
471+
logger.info("Re-armed wired device autoconnect")
458472
except Exception as exc:
459473
logger.warning("Wired autoconnect re-arm failed (non-fatal): %s", exc)
460474

@@ -2139,8 +2153,9 @@ async def _async_disconnect_ethernet(self) -> None:
21392153
await asyncio.sleep(0.5)
21402154
if not await self._is_ethernet_connected():
21412155
break
2142-
# NM latches Device.Autoconnect off here; _ensure_wired_autoconnect re-arms it.
2143-
logger.info("Ethernet disconnected (autoconnect now latched off by NM)")
2156+
# Device.Autoconnect dies with NM; the profile flag is what survives.
2157+
await self._set_wired_profiles_autoconnect(False)
2158+
logger.info("Ethernet disconnected (wired profiles autoconnect off)")
21442159
except Exception as exc:
21452160
logger.error("Failed to disconnect ethernet: %s", exc)
21462161

BlocksScreen/lib/panels/widgets/keyboardPage.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -396,15 +396,15 @@ def _setup_numpad(self) -> None:
396396
grid.addWidget(btn, idx // 3, idx % 3)
397397
self._numpad_digits.append(btn)
398398

399+
# Bottom row keeps the digit grid: "." left, "0" centred under 8/5/2.
399400
zero = self._create_numpad_button("0", "np_0")
400401
zero.setProperty("position", "down")
401-
grid.addWidget(zero, 3, 0, 1, 2)
402+
grid.addWidget(zero, 3, 1)
402403
self._numpad_digits.append(zero)
403404

404-
# Bottom row is "0 ." like a phone pad; the right column is delete/enter.
405405
self.np_dot = self._create_numpad_button(".", "np_dot")
406-
self.np_dot.setProperty("position", "right")
407-
grid.addWidget(self.np_dot, 3, 2)
406+
self.np_dot.setProperty("position", "left")
407+
grid.addWidget(self.np_dot, 3, 0)
408408

409409
self.np_delete = self._create_numpad_icon(
410410
"np_delete", ":/dialog/media/btn_icons/no.svg"

tests/network/test_worker_unit.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,6 +2142,71 @@ def test_calls_state_and_connectivity(self, qapp):
21422142
w._async_load_saved_networks.assert_awaited_once()
21432143

21442144

2145+
class TestWiredProfilesAutoconnect:
2146+
"""Device.Autoconnect dies on NM restart; only the profile flag persists."""
2147+
2148+
@staticmethod
2149+
def _wire_profiles(w, conn_type="802-3-ethernet", autoconnect=True):
2150+
nm_settings_proxy = AsyncProxyMock(
2151+
list_connections=AsyncMock(return_value=["/conn/eth"])
2152+
)
2153+
w._nm_settings = _ProxyFactory(nm_settings_proxy)
2154+
settings = {
2155+
"connection": {
2156+
"type": ("s", conn_type),
2157+
"autoconnect": ("b", autoconnect),
2158+
"timestamp": ("t", 123),
2159+
},
2160+
"ipv4": {"method": ("s", "auto")},
2161+
}
2162+
w._gather_settings = AsyncMock(return_value=[("/conn/eth", settings)])
2163+
conn_proxy = AsyncProxyMock(update=AsyncMock())
2164+
w._conn_settings = lambda path: conn_proxy
2165+
return conn_proxy
2166+
2167+
@pytest.mark.asyncio
2168+
async def test_disables_wired_profile(self, qapp):
2169+
w = _make_worker(qapp)
2170+
conn = self._wire_profiles(w, autoconnect=True)
2171+
await w._set_wired_profiles_autoconnect(False)
2172+
props = conn.update.await_args[0][0]
2173+
assert props["connection"]["autoconnect"] == ("b", False)
2174+
2175+
@pytest.mark.asyncio
2176+
async def test_strips_timestamp_nm_will_not_accept(self, qapp):
2177+
w = _make_worker(qapp)
2178+
conn = self._wire_profiles(w, autoconnect=True)
2179+
await w._set_wired_profiles_autoconnect(False)
2180+
assert "timestamp" not in conn.update.await_args[0][0]["connection"]
2181+
2182+
@pytest.mark.asyncio
2183+
async def test_reenables_wired_profile(self, qapp):
2184+
w = _make_worker(qapp)
2185+
conn = self._wire_profiles(w, autoconnect=False)
2186+
await w._set_wired_profiles_autoconnect(True)
2187+
assert conn.update.await_args[0][0]["connection"]["autoconnect"] == ("b", True)
2188+
2189+
@pytest.mark.asyncio
2190+
async def test_skips_when_already_correct(self, qapp):
2191+
w = _make_worker(qapp)
2192+
conn = self._wire_profiles(w, autoconnect=True)
2193+
await w._set_wired_profiles_autoconnect(True)
2194+
conn.update.assert_not_awaited()
2195+
2196+
@pytest.mark.asyncio
2197+
async def test_ignores_non_ethernet_profiles(self, qapp):
2198+
w = _make_worker(qapp)
2199+
conn = self._wire_profiles(w, conn_type="802-11-wireless", autoconnect=True)
2200+
await w._set_wired_profiles_autoconnect(False)
2201+
conn.update.assert_not_awaited()
2202+
2203+
@pytest.mark.asyncio
2204+
async def test_exception_is_non_fatal(self, qapp):
2205+
w = _make_worker(qapp)
2206+
w._nm_settings = MagicMock(side_effect=RuntimeError("boom"))
2207+
await w._set_wired_profiles_autoconnect(False) # must not raise
2208+
2209+
21452210
class TestEnsureWiredAutoconnect:
21462211
def test_no_wired_device_returns_early(self, qapp):
21472212
w = _make(qapp, wired=False)
@@ -2164,6 +2229,13 @@ def test_autoconnect_on_is_left_alone(self, qapp):
21642229
_run(w._ensure_wired_autoconnect())
21652230
wired.autoconnect.set_async.assert_not_awaited()
21662231

2232+
def test_profiles_are_rearmed_too(self, qapp):
2233+
w = _make(qapp)
2234+
w._set_wired_profiles_autoconnect = AsyncMock()
2235+
_wire(w, wired_proxy=AsyncProxyMock(state=30, autoconnect=False))
2236+
_run(w._ensure_wired_autoconnect())
2237+
w._set_wired_profiles_autoconnect.assert_awaited_once_with(True)
2238+
21672239
def test_exception_is_non_fatal(self, qapp):
21682240
w = _make(qapp)
21692241
w._generic = MagicMock(side_effect=RuntimeError("boom"))
@@ -2276,6 +2348,17 @@ def test_calls_disconnect(self, qapp):
22762348
wired.disconnect.assert_awaited_once()
22772349
w._deactivate_all_vlans.assert_awaited_once()
22782350

2351+
def test_persists_choice_in_the_profile(self, qapp):
2352+
w = _make(qapp)
2353+
wired = AsyncProxyMock()
2354+
wired.disconnect = AsyncMock()
2355+
_wire(w, wired_proxy=wired)
2356+
w._is_ethernet_connected = AsyncMock(return_value=False)
2357+
w._deactivate_all_vlans = AsyncMock()
2358+
w._set_wired_profiles_autoconnect = AsyncMock()
2359+
_run(w._async_disconnect_ethernet())
2360+
w._set_wired_profiles_autoconnect.assert_awaited_once_with(False)
2361+
22792362

22802363
class TestConnectEthernetAsync:
22812364
def test_no_wired_path_emits_error(self, qapp):

0 commit comments

Comments
 (0)