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 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
@@ -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
+ ![Ubuntu Start app icon](data/se.nylander.UbuntuStart.png)
28
+
29
+ ![Ubuntu Start welcome screen in English](screenshots/welcome-en.png)
30
+
31
+ [Swedish welcome screen](screenshots/welcome-sv.png) · [More screenshots](screenshots/)
32
+
33
+ ![Search results for printer troubleshooting](screenshots/search-en.png)
34
+
35
+ ![Step-by-step Wi-Fi system guide](screenshots/system-guides-en.png)
36
+
37
+ ![Accessibility instructions for larger text](screenshots/accessibility-en.png)
38
+
39
+ ![Read-only network and hardware overview](screenshots/network-hardware-en.png)
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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ubuntu-start = ubuntu_start:main