ubuntu-start 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- guide_search.py +39 -0
- system_diagnostics.py +365 -0
- ubuntu_start-0.3.1.data/data/share/icons/hicolor/512x512/apps/se.nylander.UbuntuStart.png +0 -0
- ubuntu_start-0.3.1.data/data/share/locale/sv/LC_MESSAGES/ubuntu-start.mo +0 -0
- ubuntu_start-0.3.1.data/data/share/metainfo/se.nylander.UbuntuStart.metainfo.xml +41 -0
- ubuntu_start-0.3.1.dist-info/METADATA +132 -0
- ubuntu_start-0.3.1.dist-info/RECORD +12 -0
- ubuntu_start-0.3.1.dist-info/WHEEL +5 -0
- ubuntu_start-0.3.1.dist-info/entry_points.txt +2 -0
- ubuntu_start-0.3.1.dist-info/licenses/LICENSE +675 -0
- ubuntu_start-0.3.1.dist-info/top_level.txt +3 -0
- ubuntu_start.py +856 -0
guide_search.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Search normalized guide entries without GTK dependencies."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
import unicodedata
|
|
6
|
+
|
|
7
|
+
GuideSearchEntry = tuple[str, int, str, str, str]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _normalize_token(token: str) -> str:
|
|
11
|
+
decomposed = unicodedata.normalize("NFKD", token.casefold())
|
|
12
|
+
return "".join(character for character in decomposed if character.isalnum())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def find_guide_matches(
|
|
16
|
+
query: str,
|
|
17
|
+
entries: Iterable[GuideSearchEntry],
|
|
18
|
+
) -> list[GuideSearchEntry]:
|
|
19
|
+
"""Return entries containing every query term in their visible text.
|
|
20
|
+
|
|
21
|
+
Entries contain a guide kind, its index, the guide title, a step title,
|
|
22
|
+
and the step description. Matching is case-insensitive, ignores accents
|
|
23
|
+
and punctuation, and accepts terms in any order.
|
|
24
|
+
"""
|
|
25
|
+
terms = [_normalize_token(term) for term in query.split()]
|
|
26
|
+
terms = [term for term in terms if term]
|
|
27
|
+
if not terms:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
matches = []
|
|
31
|
+
for entry in entries:
|
|
32
|
+
searchable = " ".join(
|
|
33
|
+
_normalize_token(word)
|
|
34
|
+
for value in entry[2:]
|
|
35
|
+
for word in value.casefold().split()
|
|
36
|
+
)
|
|
37
|
+
if all(term in searchable for term in terms):
|
|
38
|
+
matches.append(entry)
|
|
39
|
+
return matches
|
system_diagnostics.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Read-only system diagnostics used by Ubuntu Start."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import ipaddress
|
|
6
|
+
import platform
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.request
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_nmcli_device_status(output: str) -> list[tuple[str, str, str]]:
|
|
17
|
+
"""Parse `nmcli -t -f DEVICE,TYPE,STATE device status` output."""
|
|
18
|
+
devices = []
|
|
19
|
+
for line in output.splitlines():
|
|
20
|
+
fields = line.split(":", 2)
|
|
21
|
+
if len(fields) != 3:
|
|
22
|
+
continue
|
|
23
|
+
device, device_type, state = (field.strip() for field in fields)
|
|
24
|
+
if not device or not device_type or not state:
|
|
25
|
+
continue
|
|
26
|
+
if device.casefold() == "device":
|
|
27
|
+
continue
|
|
28
|
+
if device == "lo" or device_type.casefold() == "loopback":
|
|
29
|
+
continue
|
|
30
|
+
devices.append((device, device_type.casefold(), state.casefold()))
|
|
31
|
+
return devices
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read_network_devices(timeout: float = 3.0) -> tuple[list[tuple[str, str, str]], str | None]:
|
|
35
|
+
"""Read local NetworkManager device state without probing the network."""
|
|
36
|
+
if shutil.which("nmcli") is None:
|
|
37
|
+
return [], "not-installed"
|
|
38
|
+
|
|
39
|
+
env = os.environ.copy()
|
|
40
|
+
env.update({"LANG": "C", "LC_ALL": "C"})
|
|
41
|
+
env.pop("LANGUAGE", None)
|
|
42
|
+
try:
|
|
43
|
+
result = subprocess.run(
|
|
44
|
+
["nmcli", "--terse", "--fields", "DEVICE,TYPE,STATE", "device", "status"],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
check=False,
|
|
47
|
+
text=True,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
env=env,
|
|
50
|
+
)
|
|
51
|
+
except subprocess.TimeoutExpired:
|
|
52
|
+
return [], "timeout"
|
|
53
|
+
except OSError:
|
|
54
|
+
return [], "unavailable"
|
|
55
|
+
|
|
56
|
+
if result.returncode != 0:
|
|
57
|
+
return [], "unavailable"
|
|
58
|
+
return parse_nmcli_device_status(result.stdout), None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_bluetooth_controller_status(output: str) -> str:
|
|
62
|
+
"""Parse only controller presence and power state from bluetoothctl output."""
|
|
63
|
+
has_controller = False
|
|
64
|
+
powered = None
|
|
65
|
+
for line in output.splitlines():
|
|
66
|
+
stripped = line.strip()
|
|
67
|
+
if stripped.startswith("Controller "):
|
|
68
|
+
has_controller = True
|
|
69
|
+
key, separator, value = stripped.partition(":")
|
|
70
|
+
if separator and key.casefold() == "powered":
|
|
71
|
+
state = value.strip().casefold()
|
|
72
|
+
if state in {"yes", "no"}:
|
|
73
|
+
powered = state == "yes"
|
|
74
|
+
if not has_controller:
|
|
75
|
+
return "no-controller"
|
|
76
|
+
if powered is None:
|
|
77
|
+
return "unknown"
|
|
78
|
+
return "powered" if powered else "off"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def read_bluetooth_status(
|
|
82
|
+
timeout: float = 3.0,
|
|
83
|
+
controller_path: str | Path = "/sys/class/bluetooth",
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Read local Bluetooth controller power state without scanning for devices."""
|
|
86
|
+
if shutil.which("bluetoothctl") is None:
|
|
87
|
+
return "not-installed"
|
|
88
|
+
if not any(Path(controller_path).glob("hci*")):
|
|
89
|
+
return "no-controller"
|
|
90
|
+
result = _run_local(["bluetoothctl", "show"], timeout)
|
|
91
|
+
if result is None:
|
|
92
|
+
return "timeout-or-unavailable"
|
|
93
|
+
if result.returncode != 0:
|
|
94
|
+
return "unavailable"
|
|
95
|
+
return parse_bluetooth_controller_status(result.stdout)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_wpctl_volume(output: str) -> tuple[int, bool] | None:
|
|
99
|
+
"""Parse a volume percentage and mute flag from `wpctl get-volume`."""
|
|
100
|
+
match = re.search(r"\bVolume:\s*([0-9]+(?:\.[0-9]+)?)", output, re.IGNORECASE)
|
|
101
|
+
if not match:
|
|
102
|
+
return None
|
|
103
|
+
percent = round(float(match.group(1)) * 100)
|
|
104
|
+
muted = bool(re.search(r"\[MUTED\]", output, re.IGNORECASE))
|
|
105
|
+
return percent, muted
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_pactl_volume(output: str) -> int | None:
|
|
109
|
+
"""Parse the first channel percentage from `pactl get-*-volume`."""
|
|
110
|
+
match = re.search(r"\b([0-9]{1,3})%", output)
|
|
111
|
+
return int(match.group(1)) if match else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_pactl_mute(output: str) -> bool | None:
|
|
115
|
+
match = re.search(r"\bMute:\s*(yes|no)\b", output, re.IGNORECASE)
|
|
116
|
+
return match.group(1).casefold() == "yes" if match else None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def read_audio_status(timeout: float = 2.0) -> dict[str, tuple[int, bool] | None | str]:
|
|
120
|
+
"""Read default speaker and microphone volume/mute state without changing it."""
|
|
121
|
+
wpctl = shutil.which("wpctl")
|
|
122
|
+
pactl = shutil.which("pactl")
|
|
123
|
+
result: dict[str, tuple[int, bool] | None | str] = {
|
|
124
|
+
"output": None, "input": None, "error": None,
|
|
125
|
+
}
|
|
126
|
+
if wpctl:
|
|
127
|
+
for key, endpoint in (("output", "@DEFAULT_AUDIO_SINK@"), ("input", "@DEFAULT_AUDIO_SOURCE@")):
|
|
128
|
+
response = _run_local([wpctl, "get-volume", endpoint], timeout)
|
|
129
|
+
if response and response.returncode == 0:
|
|
130
|
+
result[key] = parse_wpctl_volume(response.stdout)
|
|
131
|
+
if pactl:
|
|
132
|
+
for key, kind, endpoint in (
|
|
133
|
+
("output", "sink", "@DEFAULT_SINK@"),
|
|
134
|
+
("input", "source", "@DEFAULT_SOURCE@"),
|
|
135
|
+
):
|
|
136
|
+
if result[key] is not None:
|
|
137
|
+
continue
|
|
138
|
+
volume = _run_local([pactl, f"get-{kind}-volume", endpoint], timeout)
|
|
139
|
+
mute = _run_local([pactl, f"get-{kind}-mute", endpoint], timeout)
|
|
140
|
+
percent = parse_pactl_volume(volume.stdout) if volume and volume.returncode == 0 else None
|
|
141
|
+
muted = parse_pactl_mute(mute.stdout) if mute and mute.returncode == 0 else None
|
|
142
|
+
if percent is not None and muted is not None:
|
|
143
|
+
result[key] = (percent, muted)
|
|
144
|
+
if not wpctl and not pactl:
|
|
145
|
+
result["error"] = "not-installed"
|
|
146
|
+
|
|
147
|
+
if result["output"] is None and result["input"] is None and result["error"] is None:
|
|
148
|
+
result["error"] = "unavailable"
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_cpu_model(cpuinfo: str) -> str | None:
|
|
153
|
+
"""Return a useful CPU model from Linux procfs content."""
|
|
154
|
+
for line in cpuinfo.splitlines():
|
|
155
|
+
key, separator, value = line.partition(":")
|
|
156
|
+
if separator and key.strip().casefold() in {"model name", "hardware", "processor"}:
|
|
157
|
+
candidate = value.strip()
|
|
158
|
+
if candidate and candidate.casefold() not in {"unknown", "unknown processor"}:
|
|
159
|
+
if key.strip().casefold() == "processor" and candidate.isdigit():
|
|
160
|
+
continue
|
|
161
|
+
return candidate
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parse_memory_kib(meminfo: str) -> int | None:
|
|
166
|
+
"""Return MemTotal in KiB from Linux procfs content."""
|
|
167
|
+
for line in meminfo.splitlines():
|
|
168
|
+
key, separator, value = line.partition(":")
|
|
169
|
+
if separator and key.strip() == "MemTotal":
|
|
170
|
+
parts = value.split()
|
|
171
|
+
if parts and parts[0].isdigit():
|
|
172
|
+
return int(parts[0])
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _read(path: Path) -> str | None:
|
|
177
|
+
try:
|
|
178
|
+
return path.read_text(encoding="utf-8", errors="replace").strip() or None
|
|
179
|
+
except OSError:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _graphics_adapters(drm_path: Path) -> list[str]:
|
|
184
|
+
adapters = []
|
|
185
|
+
for device_path in sorted(drm_path.glob("card[0-9]*/device")):
|
|
186
|
+
vendor = _read(device_path / "vendor")
|
|
187
|
+
device = _read(device_path / "device")
|
|
188
|
+
if not vendor or not device:
|
|
189
|
+
continue
|
|
190
|
+
driver_link = device_path / "driver"
|
|
191
|
+
try:
|
|
192
|
+
driver = driver_link.resolve(strict=True).name
|
|
193
|
+
except OSError:
|
|
194
|
+
driver = None
|
|
195
|
+
pci_id = f"{vendor}:{device}"
|
|
196
|
+
adapters.append(f"{pci_id} ({driver})" if driver else pci_id)
|
|
197
|
+
return adapters
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def read_hardware_details(
|
|
201
|
+
cpuinfo_path: str | Path = "/proc/cpuinfo",
|
|
202
|
+
meminfo_path: str | Path = "/proc/meminfo",
|
|
203
|
+
sys_root: str | Path = "/sys",
|
|
204
|
+
) -> dict[str, str | None | list[str]]:
|
|
205
|
+
"""Read useful hardware facts while deliberately excluding serial numbers."""
|
|
206
|
+
cpuinfo = _read(Path(cpuinfo_path)) or ""
|
|
207
|
+
meminfo = _read(Path(meminfo_path)) or ""
|
|
208
|
+
sys_root = Path(sys_root)
|
|
209
|
+
product = _read(sys_root / "class/dmi/id/product_name")
|
|
210
|
+
if product and product.casefold() in {"default string", "to be filled by o.e.m.", "not specified"}:
|
|
211
|
+
product = None
|
|
212
|
+
|
|
213
|
+
memory_kib = parse_memory_kib(meminfo)
|
|
214
|
+
memory = f"{round(memory_kib / 1_048_576)} GiB" if memory_kib is not None else None
|
|
215
|
+
return {
|
|
216
|
+
"system_model": product,
|
|
217
|
+
"processor": parse_cpu_model(cpuinfo),
|
|
218
|
+
"memory": memory,
|
|
219
|
+
"graphics": _graphics_adapters(sys_root / "class/drm"),
|
|
220
|
+
"architecture": platform.machine(),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def read_storage_status(path: str | Path = "/") -> dict[str, int]:
|
|
225
|
+
"""Return local filesystem capacity in bytes."""
|
|
226
|
+
usage = shutil.disk_usage(path)
|
|
227
|
+
return {"total": usage.total, "used": usage.used, "free": usage.free}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def read_battery_status(power_supply_path: str | Path = "/sys/class/power_supply") -> list[dict[str, str | int]]:
|
|
231
|
+
"""Read charge and optional full-charge capacity estimate without device identifiers."""
|
|
232
|
+
batteries = []
|
|
233
|
+
for battery in sorted(Path(power_supply_path).glob("*") if Path(power_supply_path).exists() else []):
|
|
234
|
+
if _read(battery / "type") != "Battery":
|
|
235
|
+
continue
|
|
236
|
+
try:
|
|
237
|
+
capacity = int(_read(battery / "capacity") or "")
|
|
238
|
+
except ValueError:
|
|
239
|
+
capacity = -1
|
|
240
|
+
status = _read(battery / "status")
|
|
241
|
+
if 0 <= capacity <= 100:
|
|
242
|
+
details: dict[str, str | int] = {"capacity": capacity, "status": status or "Unknown"}
|
|
243
|
+
for full_name, design_name in (("energy_full", "energy_full_design"), ("charge_full", "charge_full_design")):
|
|
244
|
+
try:
|
|
245
|
+
full = int(_read(battery / full_name) or "")
|
|
246
|
+
design = int(_read(battery / design_name) or "")
|
|
247
|
+
except ValueError:
|
|
248
|
+
continue
|
|
249
|
+
if full > 0 and design > 0:
|
|
250
|
+
details["design_capacity_percent"] = round(full / design * 100)
|
|
251
|
+
break
|
|
252
|
+
batteries.append(details)
|
|
253
|
+
return batteries
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def check_internet_connection(timeout: float = 5.0) -> str:
|
|
257
|
+
"""Make a small HTTPS request to Ubuntu; call only after explicit user consent."""
|
|
258
|
+
request = urllib.request.Request("https://ubuntu.com/", method="HEAD", headers={"User-Agent": "Ubuntu-Start-connectivity-check"})
|
|
259
|
+
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
260
|
+
def redirect_request(self, request, response, code, message, headers, new_url):
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
opener = urllib.request.build_opener(NoRedirect())
|
|
264
|
+
try:
|
|
265
|
+
with opener.open(request, timeout=timeout) as response:
|
|
266
|
+
return f"reachable:{response.status}"
|
|
267
|
+
except urllib.error.HTTPError as error:
|
|
268
|
+
# An HTTP response, including an error status, proves the host was reached.
|
|
269
|
+
error.close()
|
|
270
|
+
return f"reachable:{error.code}"
|
|
271
|
+
except (OSError, TimeoutError) as error:
|
|
272
|
+
return f"unreachable:{error.__class__.__name__}"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def parse_default_route(output: str) -> tuple[str, str] | None:
|
|
276
|
+
"""Extract an IP default gateway and interface from `ip route` output."""
|
|
277
|
+
for line in output.splitlines():
|
|
278
|
+
fields = line.split()
|
|
279
|
+
if len(fields) >= 5 and fields[0] == "default" and "via" in fields and "dev" in fields:
|
|
280
|
+
try:
|
|
281
|
+
gateway = str(ipaddress.ip_address(fields[fields.index("via") + 1]))
|
|
282
|
+
except (ValueError, IndexError):
|
|
283
|
+
continue
|
|
284
|
+
interface = fields[fields.index("dev") + 1]
|
|
285
|
+
if interface and interface.replace("_", "").replace("-", "").replace(".", "").isalnum():
|
|
286
|
+
return gateway, interface
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def parse_neighbor_mac(output: str) -> str | None:
|
|
291
|
+
"""Return a valid MAC address from `ip neigh` output."""
|
|
292
|
+
fields = output.split()
|
|
293
|
+
if "lladdr" not in fields:
|
|
294
|
+
return None
|
|
295
|
+
try:
|
|
296
|
+
mac = fields[fields.index("lladdr") + 1].upper().replace("-", ":")
|
|
297
|
+
except IndexError:
|
|
298
|
+
return None
|
|
299
|
+
parts = mac.split(":")
|
|
300
|
+
return mac if len(parts) == 6 and all(len(part) == 2 and all(c in "0123456789ABCDEF" for c in part) for part in parts) else None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def lookup_oui_vendor(mac: str | None, databases: tuple[str, ...] = (
|
|
304
|
+
"/usr/share/ieee-data/oui.txt", "/usr/share/hwdata/oui.txt", "/usr/share/misc/oui.txt",
|
|
305
|
+
)) -> str | None:
|
|
306
|
+
"""Match a MAC prefix against an installed IEEE OUI database; never use a web lookup."""
|
|
307
|
+
if not mac:
|
|
308
|
+
return None
|
|
309
|
+
prefix = mac.upper().replace(":", "-")[:8]
|
|
310
|
+
for database in databases:
|
|
311
|
+
try:
|
|
312
|
+
lines = Path(database).read_text(encoding="utf-8", errors="replace").splitlines()
|
|
313
|
+
except OSError:
|
|
314
|
+
continue
|
|
315
|
+
for line in lines:
|
|
316
|
+
match = re.match(r"^([0-9A-Fa-f-]{8})\s+\(hex\)\s+(.+?)\s*$", line)
|
|
317
|
+
if match and match.group(1).upper() == prefix:
|
|
318
|
+
vendor = match.group(2).strip()
|
|
319
|
+
return vendor or None
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _run_local(command: list[str], timeout: float = 3.0) -> subprocess.CompletedProcess[str] | None:
|
|
324
|
+
try:
|
|
325
|
+
return subprocess.run(command, capture_output=True, text=True, check=False, timeout=timeout)
|
|
326
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def run_network_diagnostics(timeout: float = 3.0) -> dict[str, str | float | None]:
|
|
331
|
+
"""Measure configured DNS and gateway latency; call only after user starts the check."""
|
|
332
|
+
result: dict[str, str | float | None] = {
|
|
333
|
+
"dns_latency_ms": None, "dns_error": None,
|
|
334
|
+
"gateway": None, "gateway_interface": None,
|
|
335
|
+
"gateway_latency_ms": None, "gateway_mac": None, "gateway_vendor": None,
|
|
336
|
+
}
|
|
337
|
+
route = _run_local(["ip", "route", "show", "default"], timeout)
|
|
338
|
+
default = parse_default_route(route.stdout) if route and route.returncode == 0 else None
|
|
339
|
+
if not default:
|
|
340
|
+
route = _run_local(["ip", "-6", "route", "show", "default"], timeout)
|
|
341
|
+
default = parse_default_route(route.stdout) if route and route.returncode == 0 else None
|
|
342
|
+
if default:
|
|
343
|
+
gateway, interface = default
|
|
344
|
+
family = "-6" if ipaddress.ip_address(gateway).version == 6 else "-4"
|
|
345
|
+
result["gateway"] = gateway
|
|
346
|
+
result["gateway_interface"] = interface
|
|
347
|
+
start = time.monotonic()
|
|
348
|
+
ping = _run_local(["ping", family, "-I", interface, "-n", "-c", "1", "-W", str(max(1, int(timeout))), gateway], timeout + 1)
|
|
349
|
+
if ping and ping.returncode == 0:
|
|
350
|
+
timing = re.search(r"time[=<]([0-9.]+)\s*ms", ping.stdout)
|
|
351
|
+
result["gateway_latency_ms"] = float(timing.group(1)) if timing else round((time.monotonic() - start) * 1000, 1)
|
|
352
|
+
neighbor = _run_local(["ip", family, "neigh", "show", "to", gateway, "dev", interface], timeout)
|
|
353
|
+
mac = parse_neighbor_mac(neighbor.stdout) if neighbor and neighbor.returncode == 0 else None
|
|
354
|
+
result["gateway_mac"] = mac
|
|
355
|
+
result["gateway_vendor"] = lookup_oui_vendor(mac)
|
|
356
|
+
else:
|
|
357
|
+
result["gateway_error"] = "no-default-route"
|
|
358
|
+
|
|
359
|
+
start = time.monotonic()
|
|
360
|
+
dns = _run_local(["getent", "ahosts", "ubuntu.com"], timeout)
|
|
361
|
+
if dns and dns.returncode == 0 and dns.stdout.strip():
|
|
362
|
+
result["dns_latency_ms"] = round((time.monotonic() - start) * 1000, 1)
|
|
363
|
+
else:
|
|
364
|
+
result["dns_error"] = "lookup-failed"
|
|
365
|
+
return result
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<component type="desktop-application">
|
|
3
|
+
<id>se.nylander.UbuntuStart</id>
|
|
4
|
+
<name>Ubuntu Start</name>
|
|
5
|
+
<summary>A friendly guide to Ubuntu setup and device troubleshooting</summary>
|
|
6
|
+
<summary xml:lang="sv">En hjälpsam guide för att komma igång med Ubuntu och felsöka enheter</summary>
|
|
7
|
+
<metadata_license>CC0-1.0</metadata_license>
|
|
8
|
+
<project_license>GPL-3.0</project_license>
|
|
9
|
+
<icon type="stock">se.nylander.UbuntuStart</icon>
|
|
10
|
+
<launchable type="desktop-id">se.nylander.UbuntuStart.desktop</launchable>
|
|
11
|
+
<developer id="se.nylander"><name>Ubuntu Start contributors</name></developer>
|
|
12
|
+
<url type="homepage">https://github.com/yeager/ubuntu-start</url>
|
|
13
|
+
<categories>
|
|
14
|
+
<category>Utility</category>
|
|
15
|
+
<category>System</category>
|
|
16
|
+
</categories>
|
|
17
|
+
<description>
|
|
18
|
+
<p>Get started with Ubuntu through step-by-step system guides, practical device troubleshooting, accessibility help and local network and hardware details. Search guides and copy a privacy-conscious system summary when asking for help.</p>
|
|
19
|
+
<p xml:lang="sv">Kom igång med Ubuntu med tydliga systemguider, praktisk felsökning, hjälpmedelsstöd och lokal information om nätverk och maskinvara. Sök i guiderna och kopiera en integritetsvänlig systemöversikt när du ber om hjälp.</p>
|
|
20
|
+
</description>
|
|
21
|
+
<releases>
|
|
22
|
+
<release version="0.3.1" date="2026-10-28">
|
|
23
|
+
<description>
|
|
24
|
+
<p>Provide an installable Debian package and improve the touchpad troubleshooting text so it displays correctly in GTK.</p>
|
|
25
|
+
<p xml:lang="sv">Lägg till ett installerbart Debianpaket och justera felsökningstexten för styrplattor så att den visas korrekt i GTK.</p>
|
|
26
|
+
</description>
|
|
27
|
+
</release>
|
|
28
|
+
<release version="0.3.0" date="2026-10-28">
|
|
29
|
+
<description>
|
|
30
|
+
<p>Search device troubleshooting and system guides together and open a matching guide directly. Add step-by-step help for app installation, accessibility, networking and everyday shortcuts, plus tips for backups and trusted software sources. Ubuntu Start asks before opening App Center because it may contact configured software sources; it never installs apps itself.</p>
|
|
31
|
+
<p xml:lang="sv">Sök bland enhetsfelsökning och systemguider på en gång och öppna rätt guide direkt. Få steg-för-steg-hjälp med appinstallation, hjälpmedel, nätverk och vanliga genvägar samt råd om säkerhetskopiering och betrodda programkällor. Ubuntu Start frågar innan App Center öppnas eftersom det kan ansluta till programkällor. Ubuntu Start installerar inte appar.</p>
|
|
32
|
+
</description>
|
|
33
|
+
</release>
|
|
34
|
+
<release version="0.2.0" date="2026-10-28">
|
|
35
|
+
<description>
|
|
36
|
+
<p>Add searchable troubleshooting guides, Ubuntu support links and a copyable system summary.</p>
|
|
37
|
+
</description>
|
|
38
|
+
</release>
|
|
39
|
+
</releases>
|
|
40
|
+
<content_rating type="oars-1.1"/>
|
|
41
|
+
</component>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ubuntu-start
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Ubuntu Desktop first-run and device troubleshooting guide
|
|
5
|
+
Author-email: Daniel Nylander <daniel@nylander.se>
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/yeager/ubuntu-start
|
|
8
|
+
Project-URL: Repository, https://github.com/yeager/ubuntu-start
|
|
9
|
+
Project-URL: Issues, https://github.com/yeager/ubuntu-start/issues
|
|
10
|
+
Keywords: ubuntu,gnome,desktop,troubleshooting,accessibility
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: X11 Applications :: GTK
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Desktop Environment
|
|
17
|
+
Classifier: Topic :: System :: Hardware
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Ubuntu Start
|
|
24
|
+
|
|
25
|
+
An Ubuntu Desktop guide for first-run setup and device troubleshooting.
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
[Swedish welcome screen](screenshots/welcome-sv.png) · [More screenshots](screenshots/)
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
[More screenshots: network diagnostics (EN)](screenshots/network-diagnostics-en.png) · [audio status (EN)](screenshots/audio-status-en.png) · [Terminal guide (EN)](screenshots/terminal-guide-en.png) · [WiFi Analyzer recommendation (EN)](screenshots/wifi-analyzer-en.png) · [help page (EN)](screenshots/help-en.png) · [hardware summary (EN)](screenshots/help-hardware-en.png)
|
|
42
|
+
|
|
43
|
+
[Swedish system guides](screenshots/system-guides-sv.png) · [network and hardware overview](screenshots/network-hardware-sv.png) · [network diagnostics](screenshots/network-diagnostics-sv.png) · [audio status](screenshots/audio-status-sv.png) · [Terminal guide](screenshots/terminal-guide-sv.png) · [WiFi Analyzer recommendation](screenshots/wifi-analyzer-sv.png) · [help page](screenshots/help-sv.png) · [hardware summary](screenshots/help-hardware-sv.png) · [accessibility settings](screenshots/accessibility-panel-en.png)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Ubuntu Start combines a first-run guide with practical device troubleshooting for Ubuntu Desktop. It is a GTK 4/libadwaita application intended for Ubuntu GNOME.
|
|
47
|
+
|
|
48
|
+
## Contributing
|
|
49
|
+
|
|
50
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for contributor guidance and the maintainer pull request review process.
|
|
51
|
+
|
|
52
|
+
## Current scope
|
|
53
|
+
|
|
54
|
+
- First steps for language, networking, updates, sound, display and privacy.
|
|
55
|
+
- Everyday tips for search, window management and Files, plus guidance on backups and choosing trusted app sources.
|
|
56
|
+
- Search device checks and step-by-step system guides together, then open a matching guide directly. Topics include sound, Bluetooth, batteries, printers, displays/graphics, keyboards, touchpads, Wi-Fi, Ethernet, cameras, accessibility and Terminal basics.
|
|
57
|
+
- Step-by-step guides for installing apps, updates, Wi-Fi, Ethernet, printers, batteries, keyboards, touchpads, larger text, screen magnification and the Orca screen reader.
|
|
58
|
+
- Accessibility guides explain larger text, zoom, screen reading, the on-screen keyboard, typing assistance and visual sound alerts, with plain-language steps before each change.
|
|
59
|
+
- A beginner-friendly Terminal guide explains the prompt, command output and errors, `pwd`, `ls`, `cd`, paths with spaces, Tab completion, command history, `--help`, `man`, `sudo` and safe ways to stop a command.
|
|
60
|
+
- Optional connection diagnostics report DNS lookup and gateway ping latency in milliseconds, plus the gateway MAC address and a local OUI vendor match. The test asks before running; it sends one DNS lookup to the configured resolver and one ping to the local gateway.
|
|
61
|
+
- Read-only Bluetooth status reports whether a controller is present and powered on without scanning for nearby devices.
|
|
62
|
+
- Read-only audio status checks the default speaker and microphone volume and mute state through PipeWire or PulseAudio without playing sounds or changing settings.
|
|
63
|
+
- Wi-Fi troubleshooting recommends [WiFi Analyzer](https://github.com/yeager/wifi-analyzer) for nearby signal levels and channel use. Opening GitHub requires confirmation.
|
|
64
|
+
- Built-in links to official Ubuntu documentation, Ask Ubuntu and the Ubuntu support forum.
|
|
65
|
+
- Read-only NetworkManager, Bluetooth, hardware and storage summaries, battery charge and estimated capacity versus original design when exposed, plus a copyable support report that omits usernames, host names, serial numbers, network addresses, Wi-Fi names and installed applications.
|
|
66
|
+
- Links into GNOME Control Center and Ubuntu's Additional Drivers tool. System changes remain in Ubuntu's own tools and require the user's confirmation.
|
|
67
|
+
- gettext-based localization with English source strings and a complete Swedish (`sv`) catalog.
|
|
68
|
+
|
|
69
|
+
## Build and run on Ubuntu
|
|
70
|
+
|
|
71
|
+
Install the platform dependencies, then build with Meson:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 gettext meson ninja-build
|
|
75
|
+
meson setup build --prefix=/usr
|
|
76
|
+
meson compile -C build
|
|
77
|
+
sudo meson install -C build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
To build an installable Debian package after installing the dependencies, run `scripts/build-deb.sh`. It creates `build/packages/ubuntu-start_0.3.1-1_all.deb`. Install it with `sudo apt install ./build/packages/ubuntu-start_0.3.1-1_all.deb`; APT resolves the declared GTK and Python runtime dependencies.
|
|
81
|
+
|
|
82
|
+
The PyPI wheel and source distribution are prepared. The initial upload needs a Trusted Publisher registered for this repository; after that, tagged releases publish automatically. Once the package is available, install the GTK platform dependencies with APT first, then install it in an isolated environment that can access system PyGObject bindings:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 gettext
|
|
86
|
+
python3 -m venv --system-site-packages .venv
|
|
87
|
+
. .venv/bin/activate
|
|
88
|
+
python -m pip install ubuntu-start
|
|
89
|
+
ubuntu-start
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For a source checkout, run `python3 src/ubuntu_start.py` to use the English source strings. To try the Swedish catalog from the checkout:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
mkdir -p build/locale/sv/LC_MESSAGES
|
|
96
|
+
msgfmt po/sv.po -o build/locale/sv/LC_MESSAGES/ubuntu-start.mo
|
|
97
|
+
LANGUAGE=sv UBUNTU_START_LOCALEDIR="$PWD/build/locale" python3 src/ubuntu_start.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Run the unit tests with `PYTHONPATH=src python3 -m unittest discover -s tests -v`. The CI workflow runs these tests and builds the app for pull requests.
|
|
101
|
+
|
|
102
|
+
NetworkManager, Bluetooth, audio, storage, battery and hardware summaries use local system information. DNS/gateway tests and the WiFi Analyzer GitHub link ask for confirmation before contacting an external service; DNS uses the configured resolver, ping targets the local gateway, and vendor matching uses a local OUI file when available. The separate internet test sends one HTTPS HEAD request to `ubuntu.com` after confirmation. Opening the Ubuntu App Center asks first because it may contact configured software sources; Ubuntu Start does not install apps. Other help pages open only when selected. No diagnostic report is uploaded, and serial numbers are not read. The app expects GTK 4, libadwaita and PyGObject.
|
|
103
|
+
|
|
104
|
+
## Validation
|
|
105
|
+
|
|
106
|
+
Release `v0.3.0` passed GitHub CI and the release workflow. The current 0.3.1 working tree passes all 41 unit tests, source-string coverage, translation checks, Python wheel/sdist builds and `twine check`. The 0.3.1 Debian package was built and installed on Ubuntu 26.10 machine `trv2`; package audit and APT dependency checks passed. A live GTK session exposed and verified a fix for search results with matching guides. Orca's AT-SPI app list detects Ubuntu Start. The network diagnostics remain unrun pending approval in the app's consent dialog. GNOME is selected as the next GDM session; TigerVNC currently uses Openbox because this Ubuntu image provides GNOME only as a Wayland session.
|
|
107
|
+
|
|
108
|
+
An earlier revision was also launched under Xvfb in English and Swedish. At that point PipeWire reported an unmuted default speaker at 100% and no default microphone. The GNOME Control Center panel list confirmed the Bluetooth, Wi-Fi, printer, display, privacy, sound and keyboard panel identifiers. The two Ubuntu driver/update utilities were installed with `dpkg -i`; Additional Drivers and Update Manager opened in Swedish, and `dpkg --audit` and `apt-get check` reported no package errors. These observations apply to that earlier revision. Xvfb does not verify physical-device behavior or hardware acceleration.
|
|
109
|
+
|
|
110
|
+
## Localization
|
|
111
|
+
|
|
112
|
+
English is the source and default language. The app uses the desktop locale to select translations; Swedish strings live in `po/sv.po`. Add more languages through gettext catalogs and list their language codes in `po/LINGUAS`. Regenerate the template with `xgettext` and validate catalogs with `msgfmt --check`.
|
|
113
|
+
|
|
114
|
+
See [po/README.md](po/README.md) for instructions on updating Swedish translations and contributing another language.
|
|
115
|
+
|
|
116
|
+
## Current limitations
|
|
117
|
+
|
|
118
|
+
Device troubleshooting guides are instructional. Optional network diagnostics run only after confirmation, use DNS for `ubuntu.com` and ping the local gateway, and do not upload a report. The app does not install packages or run privileged operations; system changes remain in Ubuntu's own tools. Settings panel names and the Additional Drivers launcher can differ across Ubuntu releases, so validate integrations on each supported release.
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Releases
|
|
122
|
+
|
|
123
|
+
Tagged versions (`v*`) are built by GitHub Actions. Releases include a source archive, an installable `.deb` package and SHA-256 checksums. The workflow also publishes the Python wheel and source distribution to PyPI after the repository is registered as a Trusted Publisher.
|
|
124
|
+
|
|
125
|
+
## Screenshots
|
|
126
|
+
|
|
127
|
+
The screenshots show the welcome page, matching printer troubleshooting steps, a Wi-Fi how-to guide, accessibility instructions and local network and hardware status.
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
Ubuntu Start is licensed under the GNU General Public License version 3. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
guide_search.py,sha256=juXK9qDryaIzPJ4OsJQQ1eMXnfkxcuh8J2uD4UmOaU0,1250
|
|
2
|
+
system_diagnostics.py,sha256=aAZ5C5ExSH9WNaTWiG_U7PFpxB6iSko9YFQxKoc_Oyo,14966
|
|
3
|
+
ubuntu_start.py,sha256=9c-90CvrCtMFpNYeBXBSz_IYpKQs7IiJOXhc61wKLwg,51154
|
|
4
|
+
ubuntu_start-0.3.1.data/data/share/icons/hicolor/512x512/apps/se.nylander.UbuntuStart.png,sha256=0ca9r6poPW6bd6GJjmb6M8vOiKuxYTdfLXFpkDFfFkI,259719
|
|
5
|
+
ubuntu_start-0.3.1.data/data/share/locale/sv/LC_MESSAGES/ubuntu-start.mo,sha256=D7DJKol2ORNaMa8sv8xJa3Pf7sfxPQp10VZYFZEeyoI,50787
|
|
6
|
+
ubuntu_start-0.3.1.data/data/share/metainfo/se.nylander.UbuntuStart.metainfo.xml,sha256=SAmb85qf7JjURaeyoo1vfnWjVkIx6ObdnCC6LOMR864,2884
|
|
7
|
+
ubuntu_start-0.3.1.dist-info/licenses/LICENSE,sha256=5X8cMguM-HmKfS_4Om-eBqM6A1hfbgZf6pfx2G24QFI,35150
|
|
8
|
+
ubuntu_start-0.3.1.dist-info/METADATA,sha256=kUCJctEVJJZegyOMbtdPgit5zwmXQbrGCPoeTu81hbY,10251
|
|
9
|
+
ubuntu_start-0.3.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
ubuntu_start-0.3.1.dist-info/entry_points.txt,sha256=sMlgHr4o4br6Vu98OD7R4_jiidXmzN2lJW59HeDbUcE,51
|
|
11
|
+
ubuntu_start-0.3.1.dist-info/top_level.txt,sha256=mV5AGCh77YnoMXUMbGD4hXESvvfFU4oTFKOE7JDDLQo,45
|
|
12
|
+
ubuntu_start-0.3.1.dist-info/RECORD,,
|