diff --git a/python_payload/main.py b/python_payload/main.py
index 4ba77cbffb0d025915727e2d298e821aba6563c0..51fb0c396be75622570185dcf80584405a06f866 100644
--- a/python_payload/main.py
+++ b/python_payload/main.py
@@ -1,118 +1,3 @@
-import time, gc
+from st3m.run import run_main
 
-ts_start = time.time()
-
-from st3m import logging
-
-log = logging.Log(__name__, level=logging.INFO)
-log.info(f"starting main")
-log.info(f"free memory: {gc.mem_free()}")
-
-import st3m
-
-from st3m.goose import Optional, List, ABCBase, abstractmethod
-
-from st3m import settings
-
-settings.load_all()
-
-from st3m.ui.view import View, ViewManager, ViewTransitionBlend
-from st3m.ui.menu import (
-    MenuItem,
-    MenuItemBack,
-    MenuItemForeground,
-    MenuItemNoop,
-)
-
-from st3m.ui.elements.menus import FlowerMenu, SimpleMenu, SunMenu
-from st3m.ui.elements import overlays
-
-log.info("import apps done")
-log.info(f"free memory: {gc.mem_free()}")
-ts_end = time.time()
-log.info(f"boot took {ts_end-ts_start} seconds")
-
-# TODO persistent settings
-from st3m.system import audio, captouch
-
-log.info("calibrating captouch, reset volume")
-captouch.calibration_request()
-audio.set_volume_dB(0)
-
-import leds
-
-leds.set_rgb(0, 255, 0, 0)
-
-vm = ViewManager(ViewTransitionBlend())
-
-from st3m.application import discover_bundles
-
-bundles = discover_bundles("/flash/sys/apps")
-
-# Build menu structure
-
-menu_settings = settings.build_menu()
-
-
-def _make_bundle_menu(kind: str) -> SimpleMenu:
-    entries: List[MenuItem] = [MenuItemBack()]
-    for bundle in bundles:
-        entries += bundle.menu_entries(kind)
-    return SimpleMenu(entries)
-
-
-menu_system = SimpleMenu(
-    [
-        MenuItemBack(),
-        MenuItemForeground("Settings", menu_settings),
-        MenuItemNoop("Disk Mode"),
-        MenuItemNoop("About"),
-        MenuItemNoop("Reboot"),
-    ],
-)
-
-menu_main = SunMenu(
-    [
-        MenuItemForeground("Badge", _make_bundle_menu("Badge")),
-        MenuItemForeground("Music", _make_bundle_menu("Music")),
-        MenuItemForeground("Apps", _make_bundle_menu("Apps")),
-        MenuItemForeground("System", menu_system),
-    ],
-)
-
-
-vm.push(menu_main)
-
-reactor = st3m.Reactor()
-
-# Set up top-level compositor (for combining viewmanager with overlays).
-compositor = overlays.Compositor(vm)
-
-
-# Tie compositor's debug overlay to setting.
-def _onoff_debug_update() -> None:
-    compositor.enabled[overlays.OverlayKind.Debug] = settings.onoff_debug.value
-
-
-_onoff_debug_update()
-settings.onoff_debug.subscribe(_onoff_debug_update)
-
-# Configure debug overlay fragments.
-debug = overlays.OverlayDebug()
-debug.add_fragment(overlays.DebugReactorStats(reactor))
-compositor.add_overlay(debug)
-
-debug_touch = overlays.OverlayCaptouch()
-
-
-# Tie compositor's debug touch overlay to setting.
-def _onoff_debug_touch_update() -> None:
-    compositor.enabled[overlays.OverlayKind.Touch] = settings.onoff_debug_touch.value
-
-
-_onoff_debug_touch_update()
-settings.onoff_debug_touch.subscribe(_onoff_debug_touch_update)
-compositor.add_overlay(debug_touch)
-
-reactor.set_top(compositor)
-reactor.run()
+run_main()
diff --git a/python_payload/st3m/run.py b/python_payload/st3m/run.py
new file mode 100644
index 0000000000000000000000000000000000000000..23e480a8f1b7e130c7a76e21bc9aba284fb343c6
--- /dev/null
+++ b/python_payload/st3m/run.py
@@ -0,0 +1,115 @@
+from st3m.reactor import Reactor, Responder
+from st3m.goose import List
+from st3m.ui.menu import MenuItem, MenuItemBack, MenuItemForeground, MenuItemNoop
+from st3m.ui.elements import overlays
+from st3m.ui.view import View, ViewManager, ViewTransitionBlend
+from st3m.ui.elements.menus import SimpleMenu, SunMenu
+from st3m.application import discover_bundles, BundleMetadata
+from st3m import settings, logging
+
+import captouch, audio, leds, gc
+
+log = logging.Log(__name__, level=logging.INFO)
+
+
+def run_responder(r: Responder) -> None:
+    """
+    Run a given Responder in the foreground, without any menu or main firmware running in the background.
+
+    This is useful for debugging trivial applications from the REPL.
+    """
+    reactor = Reactor()
+    reactor.set_top(r)
+    reactor.run()
+
+
+def _make_bundle_menu(bundles: List[BundleMetadata], kind: str) -> SimpleMenu:
+    entries: List[MenuItem] = [MenuItemBack()]
+    for bundle in bundles:
+        entries += bundle.menu_entries(kind)
+    return SimpleMenu(entries)
+
+
+def _make_compositor(reactor: Reactor, r: Responder) -> overlays.Compositor:
+    """
+    Set up top-level compositor (for combining viewmanager with overlays).
+    """
+    compositor = overlays.Compositor(r)
+
+    # Tie compositor's debug overlay to setting.
+    def _onoff_debug_update() -> None:
+        compositor.enabled[overlays.OverlayKind.Debug] = settings.onoff_debug.value
+
+    _onoff_debug_update()
+    settings.onoff_debug.subscribe(_onoff_debug_update)
+
+    # Configure debug overlay fragments.
+    debug = overlays.OverlayDebug()
+    debug.add_fragment(overlays.DebugReactorStats(reactor))
+    compositor.add_overlay(debug)
+
+    debug_touch = overlays.OverlayCaptouch()
+
+    # Tie compositor's debug touch overlay to setting.
+    def _onoff_debug_touch_update() -> None:
+        compositor.enabled[
+            overlays.OverlayKind.Touch
+        ] = settings.onoff_debug_touch.value
+
+    _onoff_debug_touch_update()
+    settings.onoff_debug_touch.subscribe(_onoff_debug_touch_update)
+    compositor.add_overlay(debug_touch)
+    return compositor
+
+
+def run_view(v: View) -> None:
+    """
+    Run a given View in the foreground, with an empty ViewManager underneath.
+
+    This is useful for debugging simple applications from the REPL.
+    """
+    vm = ViewManager(ViewTransitionBlend())
+    vm.push(v)
+    reactor = Reactor()
+    compositor = _make_compositor(reactor, vm)
+    reactor.set_top(compositor)
+    reactor.run()
+
+
+def run_main() -> None:
+    log.info(f"starting main")
+    log.info(f"free memory: {gc.mem_free()}")
+
+    captouch.calibration_request()
+    audio.set_volume_dB(0)
+    leds.set_rgb(0, 255, 0, 0)
+    leds.update()
+    bundles = discover_bundles("/flash/sys/apps")
+
+    settings.load_all()
+    menu_settings = settings.build_menu()
+    menu_system = SimpleMenu(
+        [
+            MenuItemBack(),
+            MenuItemForeground("Settings", menu_settings),
+            MenuItemNoop("Disk Mode"),
+            MenuItemNoop("About"),
+            MenuItemNoop("Reboot"),
+        ],
+    )
+    menu_main = SunMenu(
+        [
+            MenuItemForeground("Badge", _make_bundle_menu(bundles, "Badge")),
+            MenuItemForeground("Music", _make_bundle_menu(bundles, "Music")),
+            MenuItemForeground("Apps", _make_bundle_menu(bundles, "Apps")),
+            MenuItemForeground("System", menu_system),
+        ],
+    )
+    run_view(menu_main)
+
+
+__all__ = [
+    "run_responder",
+    "run_view",
+    "run_main",
+]