Skip to content

naneos.gui.integration

How the tray app fits into each operating system, without Qt.

  • where it logs,
  • how it starts at login (LaunchAgent, Run key, XDG autostart),
  • the macOS app bundle that lets Bluetooth ask for permission in its own name,
  • the Linux application menu entry.

Everything takes the home directory, the environment and (on Windows) the registry as optional arguments, so the tests never touch the real ones.

Autostart

Bases: ABC

Start the tray app when the user logs in.

Source code in src/naneos/gui/integration.py
131
132
133
134
135
136
137
138
139
140
141
class Autostart(ABC):
    """Start the tray app when the user logs in."""

    @abstractmethod
    def enabled(self) -> bool: ...

    @abstractmethod
    def enable(self) -> None: ...

    @abstractmethod
    def disable(self) -> None: ...

LinuxAutostart

Bases: Autostart

~/.config/autostart/naneos-devices.desktop (XDG autostart).

Source code in src/naneos/gui/integration.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
class LinuxAutostart(Autostart):
    """~/.config/autostart/naneos-devices.desktop (XDG autostart)."""

    def __init__(
        self,
        command: list[str],
        environ: Mapping[str, str] | None = None,
        home: Path | None = None,
    ) -> None:
        self._command = command
        self._environ = _environ(environ)
        self._home = _home(home)

    @property
    def desktop_path(self) -> Path:
        config = Path(self._environ.get("XDG_CONFIG_HOME") or self._home / ".config")
        return config / "autostart" / _DESKTOP_FILE

    def enabled(self) -> bool:
        return self.desktop_path.exists()

    def enable(self) -> None:
        _write_if_changed(self.desktop_path, _desktop_entry(self._command, True).encode())

    def disable(self) -> None:
        _unlink(self.desktop_path)

MacAutostart

Bases: Autostart

A LaunchAgent that opens the app bundle at login (open -g -a).

It takes effect at the next login. open goes through LaunchServices, so the start looks like a double click and the bundle stays the responsible app.

Source code in src/naneos/gui/integration.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
class MacAutostart(Autostart):
    """A LaunchAgent that opens the app bundle at login (`open -g -a`).

    It takes effect at the next login. `open` goes through LaunchServices, so the
    start looks like a double click and the bundle stays the responsible app.
    """

    def __init__(self, command: list[str], home: Path | None = None) -> None:
        self._command = command
        self._home = _home(home)

    @property
    def plist_path(self) -> Path:
        return self._home / "Library" / "LaunchAgents" / f"{BUNDLE_ID}.plist"

    def enabled(self) -> bool:
        return self.plist_path.exists()

    def enable(self) -> None:
        bundle = install_macos_bundle(self._command, self._home)
        agent = plistlib.dumps(
            {
                "Label": BUNDLE_ID,
                "ProgramArguments": ["/usr/bin/open", "-g", "-a", str(bundle)],
                "RunAtLoad": True,
                "LimitLoadToSessionType": "Aqua",
            },
            sort_keys=True,
        )
        _write_if_changed(self.plist_path, agent)

    def disable(self) -> None:
        _unlink(self.plist_path)

WindowsAutostart

Bases: Autostart

A value in HKCU\Software\Microsoft\Windows\CurrentVersion\Run (no admin needed).

Source code in src/naneos/gui/integration.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
class WindowsAutostart(Autostart):
    """A value in HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run (no admin needed)."""

    def __init__(self, command: list[str], registry: Any = None) -> None:
        self._command = command
        self._registry = registry

    def _winreg(self) -> Any:
        return self._registry or importlib.import_module("winreg")

    def enabled(self) -> bool:
        winreg = self._winreg()
        try:
            with winreg.OpenKey(
                winreg.HKEY_CURRENT_USER, _WINDOWS_RUN_KEY, 0, winreg.KEY_READ
            ) as k:
                value, _ = winreg.QueryValueEx(k, _WINDOWS_RUN_VALUE)
        except OSError:
            return False
        return bool(value)

    def enable(self) -> None:
        winreg = self._winreg()
        command = subprocess.list2cmdline(self._command)
        with winreg.CreateKey(winreg.HKEY_CURRENT_USER, _WINDOWS_RUN_KEY) as key:
            winreg.SetValueEx(key, _WINDOWS_RUN_VALUE, 0, winreg.REG_SZ, command)

    def disable(self) -> None:
        winreg = self._winreg()
        try:
            with winreg.OpenKey(
                winreg.HKEY_CURRENT_USER, _WINDOWS_RUN_KEY, 0, winreg.KEY_SET_VALUE
            ) as key:
                winreg.DeleteValue(key, _WINDOWS_RUN_VALUE)
        except OSError:
            pass  # already gone

current_platform()

"darwin", "win32" or "linux" (every other Unix counts as linux).

Source code in src/naneos/gui/integration.py
43
44
45
46
47
def current_platform() -> str:
    """ "darwin", "win32" or "linux" (every other Unix counts as linux)."""
    if sys.platform in ("darwin", "win32"):
        return sys.platform
    return "linux"

entry_point(platform=None)

The naneos-gui next to the running python, i.e. inside the environment that uv tool install made. That path stays the same when the tool is reinstalled.

Source code in src/naneos/gui/integration.py
86
87
88
89
90
def entry_point(platform: str | None = None) -> Path:
    """The naneos-gui next to the running python, i.e. inside the environment that
    `uv tool install` made. That path stays the same when the tool is reinstalled."""
    name = COMMAND_NAME + (".exe" if (platform or current_platform()) == "win32" else "")
    return Path(sys.executable).with_name(name)

install_integration(platform=None, command=None, environ=None, home=None)

Make the app startable from the desktop. Returns what was written.

macOS: the app bundle. Linux: the application menu entry. Windows: nothing here, the installer script makes the Start Menu shortcut.

Source code in src/naneos/gui/integration.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
def install_integration(
    platform: str | None = None,
    command: list[str] | None = None,
    environ: Mapping[str, str] | None = None,
    home: Path | None = None,
) -> list[Path]:
    """Make the app startable from the desktop. Returns what was written.

    macOS: the app bundle. Linux: the application menu entry. Windows: nothing
    here, the installer script makes the Start Menu shortcut.
    """
    platform = platform or current_platform()
    command = command or launch_command()
    if platform == "darwin":
        return [install_macos_bundle(command, home)]
    if platform == "linux":
        path = linux_menu_entry_path(environ, home)
        _write_if_changed(path, _desktop_entry(command, False).encode())
        return [path]
    return []

install_macos_bundle(command, home=None)

Write ~/Applications/Naneos Devices.app and return its path.

macOS asks for Bluetooth access on behalf of the "responsible" app, the one at the top of the process chain. A python started at login has no Info.plist with a usage text, so CoreBluetooth would kill it or never ask. So the bundle holds a small launcher that starts python as its child; the child inherits the launcher as the responsible app (see macos/launcher.c).

Idempotent: files that already hold the right content are left alone, and the bundle is only signed again when it changed or its signature is not valid. The signature is ad-hoc and the same for the same content, so an update of the package does not make macOS ask for Bluetooth access again.

Source code in src/naneos/gui/integration.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def install_macos_bundle(command: list[str], home: Path | None = None) -> Path:
    """Write ~/Applications/Naneos Devices.app and return its path.

    macOS asks for Bluetooth access on behalf of the "responsible" app, the one
    at the top of the process chain. A python started at login has no
    Info.plist with a usage text, so CoreBluetooth would kill it or never ask.
    So the bundle holds a small launcher that starts python as its child; the
    child inherits the launcher as the responsible app (see macos/launcher.c).

    Idempotent: files that already hold the right content are left alone, and the
    bundle is only signed again when it changed or its signature is not valid.
    The signature is ad-hoc and the same for the same content, so an update of
    the package does not make macOS ask for Bluetooth access again.
    """
    binary = launcher_binary()
    if not binary.exists():
        raise FileNotFoundError(f"the macOS launcher is missing from the package: {binary}")

    app = macos_bundle_path(home)
    contents = app / "Contents"
    installed_launcher = contents / "MacOS" / LAUNCHER_NAME
    script = "#!/bin/sh\nexec " + " ".join(shlex.quote(part) for part in command) + ' "$@"\n'

    # The installed launcher carries the signature of the bundle, so it never equals the
    # one in the package: copy it only for a new launcher version, not on every install.
    new_launcher = (
        not installed_launcher.exists()
        or _bundle_version(contents / "Info.plist") != LAUNCHER_VERSION
    )
    changed = [
        _write_if_changed(contents / "Info.plist", _macos_info_plist()),
        _write_if_changed(contents / "Resources" / "launch.sh", script.encode(), 0o755),
    ]
    if new_launcher:
        changed.append(_write_if_changed(installed_launcher, binary.read_bytes(), 0o755))
    icns = icon_file("icns")
    if icns.exists():
        changed.append(_write_if_changed(contents / "Resources" / icns.name, icns.read_bytes()))

    if any(changed):
        logger.info(f"Wrote the macOS app bundle {app}")
        # Files copied from a downloaded package can carry attributes codesign refuses.
        _run(["xattr", "-cr", str(app)])
    if any(changed) or not _codesign_is_valid(app):
        _run(["codesign", "--force", "--sign", "-", "--identifier", BUNDLE_ID, str(app)])
    return app

launch_command()

The command that starts the tray app: the installed script, or python -m naneos.gui when run from a checkout.

Source code in src/naneos/gui/integration.py
93
94
95
96
97
98
99
def launch_command() -> list[str]:
    """The command that starts the tray app: the installed script, or `python -m
    naneos.gui` when run from a checkout."""
    entry = entry_point()
    if entry.exists():
        return [str(entry)]
    return [sys.executable, "-m", "naneos.gui"]

log_dir(platform=None, environ=None, home=None)

Where the tray app writes naneos-devices.log.

Source code in src/naneos/gui/integration.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def log_dir(
    platform: str | None = None,
    environ: Mapping[str, str] | None = None,
    home: Path | None = None,
) -> Path:
    """Where the tray app writes naneos-devices.log."""
    platform = platform or current_platform()
    env, home = _environ(environ), _home(home)
    if platform == "darwin":
        return home / "Library" / "Logs" / "naneos-devices"
    if platform == "win32":
        base = Path(env.get("LOCALAPPDATA") or home / "AppData" / "Local")
        return base / "naneos" / "naneos-devices" / "Logs"
    return Path(env.get("XDG_STATE_HOME") or home / ".local" / "state") / "naneos-devices"