diff --git a/lib/appkit/README.md b/lib/appkit/README.md index 7eb6998..e99d95d 100644 --- a/lib/appkit/README.md +++ b/lib/appkit/README.md @@ -185,6 +185,9 @@ hiding the other. When `apps/` has no `__init__.py`, `scan()` makes one; where it can't (a read-only filesystem) it stops with an error saying to add an empty one. +When `apps/` holds an app named `home`, `appkit.run()` starts that as the +home screen instead of its own list of buttons. + `appkit.run("hello")` runs one app on its own, with the whole screen and no home screen. An app set that wants its own home screen, chrome or launcher setup builds a `Launcher` itself. Its `Shell` subclass gives apps a smaller diff --git a/lib/appkit/__init__.py b/lib/appkit/__init__.py index 22173a2..17da72a 100644 --- a/lib/appkit/__init__.py +++ b/lib/appkit/__init__.py @@ -19,6 +19,9 @@ def run(name=None, folder="apps", *, data_root=None): """Run the apps in *folder*: a home screen listing them, or app *name* alone. + The home screen is the folder's own app named ``home`` when it has one, + and otherwise appkit's list of buttons, ``appkit.home``. + It ends in the main-thread loop, `serve`, and returns the ``Launcher`` when the program quits. In a browser, where the page has the main thread, it returns the launcher at once. *data_root* moves @@ -33,10 +36,17 @@ def run(name=None, folder="apps", *, data_root=None): launcher = Launcher(app) launcher.scan(folder) if name is None: - launcher.add("home", "appkit.home") - launcher.home = "home" + _use_home(launcher) launcher.shell = Shell(launcher) launcher.boot(name) if host() != "browser": serve(app) return launcher + + +def _use_home(launcher): + """Make ``home`` the launcher's home screen: the folder's own app named + home when the scan found one, else appkit's list of buttons.""" + if "home" not in launcher.names: + launcher.add("home", "appkit.home") + launcher.home = "home" diff --git a/tests/appkit/launcher_contract.py b/tests/appkit/launcher_contract.py index dd4baf6..ccb50fe 100644 --- a/tests/appkit/launcher_contract.py +++ b/tests/appkit/launcher_contract.py @@ -304,6 +304,21 @@ def restarts_in_a_row_are_bounded(): la.stop() +@check +def run_keeps_a_scanned_home(): + import appkit + + la = L.Launcher(_app()) + la.scan(_dir + _sep + "sample_home" + _sep + "apps") + appkit._use_home(la) + expect(la.home == "home", "home is %r" % (la.home,)) + expect(la._apps["home"] == "apps.home", "the scanned home was replaced by %r" % (la._apps["home"],)) + la = L.Launcher(_app()) + la.scan(_dir + _sep + "sample_set" + _sep + "apps") + appkit._use_home(la) + expect(la._apps["home"] == "appkit.home", "a folder without home got %r" % (la._apps["home"],)) + + @check def pending_app_round_trip(): # On a board this is the real RTC memory (or /next_app), without the reset. diff --git a/tests/appkit/sample_home/apps/__init__.py b/tests/appkit/sample_home/apps/__init__.py new file mode 100644 index 0000000..4f15319 --- /dev/null +++ b/tests/appkit/sample_home/apps/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT + +# The apps folder is a package: appkit imports each app in it as apps.. diff --git a/tests/appkit/sample_home/apps/home/__init__.py b/tests/appkit/sample_home/apps/home/__init__.py new file mode 100644 index 0000000..5e1cf8a --- /dev/null +++ b/tests/appkit/sample_home/apps/home/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""A folder's own home screen, which appkit.run() starts instead of its list.""" + + +def main(scope): + pass diff --git a/tests/appkit/sample_home/apps/home/app.json b/tests/appkit/sample_home/apps/home/app.json new file mode 100644 index 0000000..7ece58b --- /dev/null +++ b/tests/appkit/sample_home/apps/home/app.json @@ -0,0 +1 @@ +{"name": "home", "title": "Home", "tile": false}