"""What happens when somebody double-clicks the executable. **One decision, stated plainly: no arguments means the screen, not the usage text.** A command-line program with no arguments conventionally prints how to use it and exits, and that convention is right for a program invoked from a shell by somebody who mistyped. It is wrong for this one, because the most common way this is started is a double-click by a person who has never seen a terminal, and answering them with an argument grammar is answering a question they did not ask. So: no arguments at all, and a console that can be read, opens the setup and then the screen. Any argument at all, or a console that cannot be read, behaves exactly as before. Nobody's script changes, and `--help` still prints help. **The distinction that makes this safe** is that this module produces a command line and hands it to `cli.main`. It does not have its own way to start a worker. Everything `main` refuses to do when typed at, it refuses to do here: a remote server without permission, a plaintext one without a second permission, a missing pairing code. This file asks questions and formats an answer; it decides nothing about what a worker may do. """ from __future__ import annotations import os import sys from typing import Any, Callable, Optional, Sequence #: Set by a launcher that has already decided. Mostly for tests, which must be #: able to exercise both paths without depending on whether pytest happens to #: be attached to a terminal. FORCE_ENV_VAR = "DISTINCT_DESKTOP" def wants_desktop( argv: Optional[Sequence[str]] = None, *, environ: Optional[dict] = None, isatty: Optional[Callable[[], bool]] = None, ) -> bool: """Whether this invocation should open the screen rather than parse flags. Three things have to be true, and the third is the one that matters. There must be no arguments, because an argument is somebody being specific and being overridden is infuriating. The console must be readable, because the setup screen asks questions and a screen that asks a question nobody can answer is a hang. And the environment override must not say otherwise, which is how a test drives either path deliberately. """ environ = os.environ if environ is None else environ forced = environ.get(FORCE_ENV_VAR) if forced == "0": return False if forced == "1": return True arguments = list(sys.argv[1:] if argv is None else argv) if arguments: return False if isatty is None: def isatty() -> bool: try: return bool(sys.stdin.isatty() and sys.stdout.isatty()) except (AttributeError, ValueError): return False return bool(isatty()) def run(*, main: Optional[Callable[..., int]] = None, settings_io: Any = None) -> int: """Ask, remember, and start. Returns the exit code the worker returns. ``settings_io`` is the `firstrun` module by default and is injectable so a test can drive the whole flow against a temporary file rather than against whatever is in the person's real configuration directory. """ from . import firstrun from .dashboard import ActivityLog from .tui import SetupApp settings_io = settings_io or firstrun if main is None: from .cli import main as main settings = settings_io.load() answered = SetupApp(settings, first_run=not settings.is_complete()).run() if answered is None: print("Nothing was started.", file=sys.stderr) return 0 settings, code = answered if not code: print("No pairing code, so nothing was started.", file=sys.stderr) return 0 where = settings_io.save(settings) if where is None: print( "These answers could not be saved, so they will be asked again next " "time. The worker still starts.", file=sys.stderr, ) argv = settings_io.to_argv(settings, code) + ["--dashboard"] # Everything the worker prints from here is bound for the screen, and the # screen does not exist yet: pairing, the catalogue, the approval question # and possibly a long download all happen first. So the log mirrors to the # real stderr until the dashboard takes the terminal over, and somebody # watching a slow start sees it happening rather than a blank window. log = ActivityLog(mirror=sys.stderr) log._distinct_activity_log = log # noqa: SLF001 - see the note below previous = sys.stderr sys.stderr = log try: return int(main(argv)) finally: sys.stderr = previous # `cli._run_with_dashboard` finds the log by asking `sys.stderr` for # `_distinct_activity_log`, rather than by taking a parameter. That is the one # piece of indirection here worth defending: `main` has a long argument list # already, the log is a property of how this process was launched rather than # of what the worker was asked to do, and threading it through would put a # display concern into every signature between here and there. The attribute is # set on the object itself, so a `sys.stderr` that is not one of these simply # answers None and the dashboard makes its own.