diff --git a/python_payload/apps/gr33nhouse/__init__.py b/python_payload/apps/gr33nhouse/__init__.py
index e3089becf3ff3b8363144b6966718e2377998e14..d5b24259fd9f468a4aac338e8002016587542014 100644
--- a/python_payload/apps/gr33nhouse/__init__.py
+++ b/python_payload/apps/gr33nhouse/__init__.py
@@ -34,30 +34,39 @@ class Gr33nhouseApp(Application):
         self._sc.set_item_count(3)
         self.acceptSdCard = False
 
-        self.state = ViewState.CONTENT
+    def on_enter(self, vm):
+        super().on_enter(vm)
+        self.update_state()
+
+    def update_state(self):
+        if not self.acceptSdCard and sd_card_unreliable():
+            self.state = ViewState.BAD_SDCARD
+        elif not st3m.wifi.is_connected():
+            self.state = ViewState.NO_INTERNET
+        else:
+            self.state = ViewState.CONTENT
 
     def on_exit(self) -> bool:
         # request thinks after on_exit
         return True
 
     def draw(self, ctx: Context) -> None:
+        ctx.rgb(*colours.BLACK)
+        ctx.rectangle(
+            -120.0,
+            -120.0,
+            240.0,
+            240.0,
+        ).fill()
+
+        ctx.rgb(*colours.WHITE)
+        ctx.font = "Camp Font 3"
+        ctx.font_size = 24
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.MIDDLE
+
         if self.state == ViewState.BAD_SDCARD:
-            ctx.move_to(0, 0)
-            ctx.rgb(*colours.BLACK)
-            ctx.rectangle(
-                -120.0,
-                -120.0,
-                240.0,
-                240.0,
-            ).fill()
-
-            ctx.save()
-            ctx.rgb(*colours.WHITE)
-            ctx.font = "Camp Font 3"
             ctx.font_size = 18
-            ctx.text_align = ctx.CENTER
-            ctx.text_baseline = ctx.MIDDLE
-
             ctx.move_to(0, -15)
             ctx.text("Unreliable SD card detected!")
             ctx.move_to(0, 5)
@@ -70,25 +79,8 @@ class Gr33nhouseApp(Application):
             ctx.move_to(0, 55)
             ctx.text("continue anyway.")
 
-            ctx.restore()
-            return
-        if self.state == ViewState.NO_INTERNET:
-            ctx.move_to(0, 0)
-            ctx.rgb(*colours.BLACK)
-            ctx.rectangle(
-                -120.0,
-                -120.0,
-                240.0,
-                240.0,
-            ).fill()
-
-            ctx.save()
-            ctx.rgb(*colours.WHITE)
-            ctx.font = "Camp Font 3"
+        elif self.state == ViewState.NO_INTERNET:
             ctx.font_size = 24
-            ctx.text_align = ctx.CENTER
-            ctx.text_baseline = ctx.MIDDLE
-
             ctx.move_to(0, -15)
             ctx.text("Connecting..." if st3m.wifi.is_connecting() else "No internet")
 
@@ -99,84 +91,20 @@ class Gr33nhouseApp(Application):
             ctx.move_to(0, 55)
             ctx.text("enter Wi-Fi settings.")
 
-            ctx.restore()
-            return
-
-        self.background.draw(ctx)
-        ctx.save()
-
-        ctx.gray(1.0)
-        ctx.rectangle(
-            -120.0,
-            -15.0,
-            240.0,
-            30.0,
-        ).fill()
-
-        ctx.translate(0, -30 * self._sc.current_position())
-
-        offset = 0
-
-        ctx.font = "Camp Font 3"
-        ctx.font_size = 24
-        ctx.text_align = ctx.CENTER
-        ctx.text_baseline = ctx.MIDDLE
-
-        for idx, item in enumerate(self.items):
-            if idx == self._sc.target_position():
-                ctx.gray(0.0)
-            else:
-                ctx.gray(1.0)
-
-            ctx.move_to(0, offset)
-            ctx.text(item)
-            offset += 30
-
-        ctx.restore()
-
     def think(self, ins: InputState, delta_ms: int) -> None:
         super().think(ins, delta_ms)
         self._sc.think(ins, delta_ms)
 
         self.background.think(ins, delta_ms)
 
-        if not self.acceptSdCard:
-            if sd_card_unreliable():
-                self.state = ViewState.BAD_SDCARD
-            else:
-                self.acceptSdCard = True
-
-        if self.state == ViewState.BAD_SDCARD and self.input.buttons.app.middle.pressed:
-            self.state = ViewState.CONTENT
-            self.acceptSdCard = True
-            return
-
         if self.state == ViewState.BAD_SDCARD:
-            return
-
-        if not self.is_active():
-            return
-
-        if not st3m.wifi.is_connected():
-            self.state = ViewState.NO_INTERNET
+            if self.input.buttons.app.middle.pressed:
+                self.state = ViewState.CONTENT
+                self.acceptSdCard = True
+        elif self.state == ViewState.NO_INTERNET:
             if self.input.buttons.app.middle.pressed:
                 st3m.wifi.run_wifi_settings(self.vm)
-            return
         else:
-            self.state = ViewState.CONTENT
+            self.vm.replace(AppList())
 
-        if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
-            self._sc.scroll_left()
-        elif (
-            self.input.buttons.app.right.pressed
-            or self.input.buttons.app.right.repeated
-        ):
-            self._sc.scroll_right()
-        elif self.input.buttons.app.middle.pressed:
-            pos = self._sc.target_position()
-            if pos == 0:
-                self.vm.push(AppList())
-            elif pos == 1:
-                self.vm.push(RecordView())
-            elif pos == 2:
-                self.vm.push(ManualInputView())
+        self.update_state()
diff --git a/python_payload/apps/gr33nhouse/applist.py b/python_payload/apps/gr33nhouse/applist.py
index 688b48e9dcf5c84979ab27da019a6b54cbf05e4e..99788f1a55153426959a9795c666d4c9982c7fa9 100644
--- a/python_payload/apps/gr33nhouse/applist.py
+++ b/python_payload/apps/gr33nhouse/applist.py
@@ -7,8 +7,12 @@ from ctx import Context
 from math import sin
 import urequests
 import time
+import os
+import json
 from .background import Flow3rView
 from .confirmation import ConfirmationView
+from .background import ColorTheme, broken_col, update_col, installed_col, color_themes
+from .manual import ManualInputView
 
 
 class ViewState(Enum):
@@ -18,19 +22,291 @@ class ViewState(Enum):
     LOADED = 4
 
 
+class AppDB:
+    class App:
+        class Version:
+            def __init__(self, version, url_dict=None):
+                # None indicates unknown value for most fields
+
+                # version of the app as in its flow3r.toml
+                self.version = version
+                # urls
+                if url_dict is not None:
+                    self.zip_url = url_dict["downloadUrl"]
+                    self.tar_url = url_dict["tarDownloadUrl"]
+                # if this version is installed
+                self.installed = None
+                # if this version is considered an update
+                self.update = None
+
+                # this version of the app was tested by flow3r team
+                self.tested = False
+                # which version was tested
+                self.tested_version = None
+                # test result: is app broken?
+                self.broken = None
+
+                # true if app is patch by flow3r team
+                self.patch = None
+                # which original version this patch forks
+                self.patch_base_version = None
+
+        def __init__(self, raw_app):
+            self.raw_app = raw_app
+            self.processed = False
+
+        def process(self, toml_cache):
+            if self.processed:
+                return
+            raw_app = self.raw_app
+            self.category = raw_app.get("menu")
+            for field in ["name", "author", "description", "stars", "featured"]:
+                setattr(self, field, raw_app.get(field))
+            slug_bits = raw_app.get("repoUrl").split("/")
+            self.slug = "-".join([slug_bits[-2], slug_bits[-1].lower()])
+
+            # get latest original version
+            orig = self.Version(self.raw_app["version"], url_dict=self.raw_app)
+            self.available_versions = [orig]
+            self.orig = orig
+
+            # check what version is installed
+            installed = None
+            self.installed_version = None
+            self.installed = False
+            self.installed_path = None
+            for app_dir in ["/sd/apps/", "/sys/flash/apps/"]:
+                path = app_dir + self.slug
+                app_installed = toml_cache.get(path)
+                if not app_installed:
+                    continue
+                if not os.path.exists(path):
+                    print(f"app in database but not in filesystem: {path}")
+                    continue
+                try:
+                    installed = self.Version(app_installed["metadata"]["version"])
+                except:
+                    print("parsing installed version in toml.cache failed")
+                installed.patch = "patch_source" in app_installed
+                self.installed_path = path
+                self.installed_version = installed
+                self.installed = True
+                break
+
+            # check for flow3r team status flags/patched version
+            patch = None
+            self.broken = None
+            if (status := raw_app.get("status")) is not None:
+                if (tested_version := status.get("tested_version")) is not None:
+                    orig.tested_version = tested_version
+                    orig.tested = orig.tested_version == orig.version
+                if (broken := status.get("broken")) is not None:
+                    # if we specify a tested version, a new version shouldn't
+                    # be flagged as broken.
+                    if orig.tested_version is None or orig.tested:
+                        orig.broken = broken
+                if (app_patch := status.get("patch")) is not None:
+                    version = app_patch.get("version")
+                    patch = self.Version(version, url_dict=app_patch)
+                    patch.patch = True
+                    patch.patch_base_version = orig.tested_version
+                    # order of list is display order, so if the original
+                    # isn't broken we wanna recommend it frist, else
+                    # we default to the patched version
+                    self.broken = False
+                    if orig.broken:
+                        self.available_versions.insert(0, patch)
+                    else:
+                        self.available_versions.append(patch)
+                else:
+                    self.broken = orig.broken
+            self.patch = patch
+
+            # check for style
+            self.colors = ColorTheme.from_style(raw_app["style"], self.category)
+
+            # check if updates are available
+            self.processed = True
+
+            self._update_versions()
+
+        def _update_versions(self):
+            installed = self.installed_version
+            patch = self.patch
+            orig = self.orig
+
+            orig.installed = False
+            orig.update = False
+            if patch:
+                patch.installed = False
+                patch.update = False
+
+            if not installed:
+                self.installed = False
+                self.update_available = None
+                return
+
+            if patch is None:
+                orig.installed = orig.version == installed.version
+                if orig.version > installed.version:
+                    self.update_available = True
+            else:
+                if installed.patch:
+                    orig.installed = False
+                    patch.installed = patch.version == installed.version
+
+                    # option 1: new version of original came out
+                    orig.update = orig.version > patch.patch_base_version
+                    # option 2: new version of patch came out
+                    patch.update = patch.version > installed.version
+                else:
+                    orig.installed = orig.version == installed.version
+                    patch.installed = False
+
+                    # option 1: new version of original came out
+                    orig.update = orig.version > installed.version
+                    # option 2: patch for installed version came out
+                    patch.update = patch.patch_base_version == installed.version
+
+            self.installed = True
+            self.update_available = any(
+                [version.update for version in self.available_versions]
+            )
+
+        def update_installed_version(self, version):
+            if False:
+                print(f"updating {self.slug}:")
+                print(f"  previous at {self.installed_path}:")
+                for v in self.available_versions:
+                    print(
+                        f'    {"patch" if v.patch else "orig"} v{v.version} {"installed" if v.installed else ""}'
+                    )
+                if self.installed_version is not None:
+                    v = self.installed_version
+                    print(
+                        f'    installed: {"patch" if v.patch else "orig"} v{v.version}'
+                    )
+            self.installed_version = version
+            self._update_versions()
+            if False:
+                print(f"  now at {self.installed_path}:")
+                for v in self.available_versions:
+                    print(
+                        f'    {"patch" if v.patch else "orig"} v{v.version} {"installed" if v.installed else ""}'
+                    )
+                if self.installed_version is not None:
+                    v = self.installed_version
+                    print(
+                        f'    installed: {"patch" if v.patch else "orig"} v{v.version}'
+                    )
+
+    class Category:
+        def __init__(self, name, db):
+            self.name = name
+            self._db = db
+            self.apps = []
+
+        def add_app(self, app):
+            if self.name != app.category:
+                print("app seems to be in wrong category")
+            self.apps.append(app)
+
+        def scan_all(self):
+            db = self._db
+            for x in range(db._process_index, len(db._raw_apps)):
+                raw_app = db._unprocessed_apps[x]
+                if raw_app.get("menu") == self.name:
+                    db.add_raw_app(raw_app)
+
+            db.applist_sort(self.apps)
+
+    @staticmethod
+    def applist_sort_key(app):
+        if app.update_available:
+            return -1
+        elif app.installed:
+            return 1
+        return 0
+
+    @classmethod
+    def applist_sort(cls, apps):
+        apps.sort(key=cls.applist_sort_key)
+
+    def __init__(self, raw_apps):
+        self._raw_apps = raw_apps
+        self._process_index = 0
+        self.apps = []
+        try:
+            with open("/sd/apps/toml_cache.json") as f:
+                self._toml_cache = json.load(f)
+        except:
+            self._toml_cache = {}
+        self._done = False
+        self.categories = {}
+
+    def scan_incremental(self, increment=8):
+        if self._done:
+            return
+        while True:
+            if self._process_index >= len(self._raw_apps):
+                self.applist_sort(self.apps)
+                self._done = True
+                print("Database finished")
+                return
+            raw_app = self._raw_apps[self._process_index]
+            self._process_index += 1
+            self.add_raw_app(raw_app)
+            if increment is not None:
+                increment -= 1
+                if not increment:
+                    break
+
+    def scan_all_category_names(self):
+        for x in range(self._process_index, len(self._raw_apps)):
+            raw_app = self._raw_apps[x]
+            if (cat := raw_app.get("menu")) not in self.categories:
+                self.categories[cat] = None
+
+    def scan_all(self):
+        self.scan_incremental(None)
+
+    def add_raw_app(self, raw_app):
+        app = self.App(raw_app)
+        app.process(self._toml_cache)
+        self.apps.append(app)
+
+        # write into category list
+        cat = app.category
+        # may exist as key but contain None, see scan_all_categories
+        if (category := self.categories.get(cat)) is None:
+            category = self.Category(cat, self)
+            self.categories[cat] = category
+        category.add_app(app)
+
+
 class AppSubList(BaseView):
     _scroll_pos: float = 0.0
 
-    apps: list[Any] = []
-
     background: Flow3rView
 
-    def __init__(self, apps) -> None:
+    def __init__(self, apps, colors, hide_tags=tuple(), app_filter=None) -> None:
         super().__init__()
-        self.background = Flow3rView()
+        self.background = Flow3rView(colors)
+        self.colors = colors
         self._sc = ScrollController()
-        self.apps = apps
-        self._sc.set_item_count(len(self.apps))
+        self.hide_tags = tuple(hide_tags)
+        self.app_filter = app_filter
+        if self.app_filter:
+            self.unfiltered_apps = apps
+        else:
+            self.apps = apps
+            self._sc.set_item_count(len(apps))
+
+    def on_enter(self, vm):
+        super().on_enter(vm)
+        if self.app_filter:
+            self.apps = [x for x in self.unfiltered_apps if self.app_filter(x)]
+            self._sc.set_item_count(len(self.apps))
 
     def on_exit(self) -> bool:
         # request thinks after on_exit
@@ -42,7 +318,7 @@ class AppSubList(BaseView):
         self.background.draw(ctx)
 
         ctx.save()
-        ctx.gray(1.0)
+        ctx.rgb(*self.colors.text_bg)
         ctx.rectangle(
             -120.0,
             -15.0,
@@ -63,17 +339,43 @@ class AppSubList(BaseView):
         for idx, app in enumerate(self.apps):
             target = idx == self._sc.target_position()
             if target:
-                ctx.gray(0.0)
+                ctx.rgb(*self.colors.text_fg)
             else:
-                ctx.gray(1.0)
+                ctx.rgb(*self.colors.fg)
 
             if abs(self._sc.current_position() - idx) <= 5:
                 xpos = 0.0
-                if target and (width := ctx.text_width(app["name"])) > 220:
+                if target and (width := ctx.text_width(app.name)) > 220:
                     xpos = sin(self._scroll_pos) * (width - 220) / 2
                 ctx.move_to(xpos, offset)
-                ctx.text(app["name"])
+                ctx.text(app.name)
+
+                if target:
+                    text = None
+                    col = (0, 0, 0)
+                    if app.update_available:
+                        col = update_col
+                        text = "update"
+                    elif app.installed:
+                        col = installed_col
+                        text = "installed"
+                    elif app.broken:
+                        col = broken_col
+                        text = "broken"
+
+                    if text and not (text in self.hide_tags):
+                        ctx.save()
+                        ctx.rgb(*col)
+                        ctx.font_size = 16
+                        ctx.text_align = ctx.LEFT
+                        ctx.rel_move_to(0, -5)
+                        ctx.text(text)
+                        ctx.restore()
+
             offset += 30
+        if not self.apps:
+            ctx.rgb(*self.colors.text_fg)
+            ctx.text("(empty)")
 
         ctx.restore()
 
@@ -99,18 +401,9 @@ class AppSubList(BaseView):
         elif self.input.buttons.app.middle.pressed:
             if self.vm is None:
                 raise RuntimeError("vm is None")
-
-            app = self.apps[self._sc.target_position()]
-            url = app["tarDownloadUrl"]
-            name = app["name"]
-            author = app["author"]
-            self.vm.push(
-                ConfirmationView(
-                    url=url,
-                    name=name,
-                    author=author,
-                )
-            )
+            if self.apps:
+                app = self.apps[self._sc.target_position()]
+                self.vm.push(ConfirmationView(app))
 
 
 class AppList(BaseView):
@@ -118,15 +411,16 @@ class AppList(BaseView):
     _state: ViewState = ViewState.INITIAL
 
     items: list[Any] = ["All"]
-    category_order: list[Any] = ["Badge", "Music", "Media", "Apps", "Games", "Demos"]
+    category_order: list[Any] = ["Badge", "Music", "Games", "Media", "Apps", "Demos"]
 
     background: Flow3rView
 
     def __init__(self) -> None:
         super().__init__()
-        self.background = Flow3rView()
+        self.background = Flow3rView(ColorTheme.get("SolidBlack"))
         self._sc = ScrollController()
         self._sc.set_item_count(len(self.items))
+        self.category_prev = ""
 
     def draw(self, ctx):
         ctx.move_to(0, 0)
@@ -223,16 +517,18 @@ class AppList(BaseView):
                 self._state = ViewState.LOADING
                 print("Loading app list...")
                 res = urequests.get("https://flow3r.garden/api/apps.json")
-                self.apps = res.json()["apps"]
-
-                if self.apps == None:
+                raw_apps = res.json()["apps"]
+                if raw_apps == None:
                     print(f"Invalid JSON or no apps: {res.json()}")
                     self._state = ViewState.ERROR
                     return
 
-                categories = [app.get("menu") for app in self.apps]
-                categories = [c for c in categories if c and isinstance(c, str)]
-                categories = list(set(categories))
+                print("Initializing database...")
+                self.db = AppDB(raw_apps)
+                self.apps = self.db.apps
+
+                self.db.scan_all_category_names()
+                categories = list(self.db.categories.keys())
 
                 def sortkey(obj):
                     try:
@@ -241,11 +537,11 @@ class AppList(BaseView):
                         return len(self.category_order)
 
                 categories.sort(key=sortkey)
-                self.items = ["All"] + categories
+                print("Found categories:", categories)
+                self.items = ["All"] + categories + ["Updates", "Installed", "Seeds"]
                 self._sc.set_item_count(len(self.items))
 
                 self._state = ViewState.LOADED
-                print("App list loaded")
             except Exception as e:
                 print(f"Load failed: {e}")
                 self._state = ViewState.ERROR
@@ -261,6 +557,8 @@ class AppList(BaseView):
         if not self.is_active():
             return
 
+        self.db.scan_incremental()
+
         if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
             self._sc.scroll_left()
             self._scroll_pos = 0.0
@@ -273,9 +571,37 @@ class AppList(BaseView):
         elif self.input.buttons.app.middle.pressed:
             if self.vm is None:
                 raise RuntimeError("vm is None")
-            if self._sc.target_position():
-                category = self.items[self._sc.target_position()]
-                apps = [app for app in self.apps if app.get("menu") == category]
+            category = self.items[self._sc.target_position()]
+            colors = ColorTheme.get(category)
+            hide_tags = []
+            apps = None
+            app_filter = None
+            if category == "All":
+                self.db.scan_all()
+                apps = self.apps
+            elif category == "Updates":
+                self.db.scan_all()
+                app_filter = lambda x: x.update_available
+                apps = self.apps
+            elif category == "Installed":
+                self.db.scan_all()
+                app_filter = lambda x: x.installed
+                apps = self.apps
+                hide_tags = ["installed"]
+            elif category == "Featured":
+                self.db.scan_all()
+                app_filter = lambda x: x.featured
+                apps = self.apps
+            elif category == "Seeds":
+                self.vm.push(ManualInputView(colors=colors))
             else:
-                apps = list(self.apps)
-            self.vm.push(AppSubList(apps=apps))
+                category = self.db.categories[category]
+                category.scan_all()
+                apps = category.apps
+            if apps is not None:
+                self.vm.push(AppSubList(apps, colors, hide_tags, app_filter))
+
+        category = self.items[self._sc.target_position()]
+        if category != self.category_prev:
+            self.category_prev = category
+            self.background.update_colors(ColorTheme.get(category))
diff --git a/python_payload/apps/gr33nhouse/background.py b/python_payload/apps/gr33nhouse/background.py
index f3a58605ca63e73f344f9f1a0302af96caa1ded7..87c9c01142056dc38d51cc40219a719470ba9c0a 100644
--- a/python_payload/apps/gr33nhouse/background.py
+++ b/python_payload/apps/gr33nhouse/background.py
@@ -4,34 +4,167 @@ from st3m.input import InputState
 from st3m.ui.view import BaseView
 from ctx import Context
 
+seed_cols = [
+    (0, 0, 1),
+    (0, 1, 1),
+    (1, 1, 0),
+    (0, 1, 0),
+    (1, 0, 1),
+]
+
+broken_col = (0, 0.4, 1)
+update_col = (0.9, 0.7, 0)
+installed_col = (0.5, 0.5, 0.5)
+
+
+def color_blend(lo, hi, blend):
+    if blend >= 1:
+        return hi
+    elif blend <= 0:
+        return lo
+    num_x = 3
+    if len(lo) > 3 or len(hi) > 3:
+        lo = list(lo) + [1]
+        hi = list(hi) + [1]
+        num_x = 4
+    return tuple([lo[x] * (1 - blend) + hi[x] * blend for x in range(num_x)])
+
+
+class FlowerTheme:
+    def __init__(self, fg, solid=False):
+        self.fg = fg
+        self.solid = solid
+
+
+class ColorTheme:
+    def __init__(self, bg, flowers, num_stars=8):
+        self.bg = bg
+        self.fg = (1, 1, 1)
+        self.text_bg = (1, 1, 1)
+        self.text_fg = (0, 0, 0)
+        self.flowers = flowers
+        self.num_stars = num_stars
+
+    @classmethod
+    def from_style(cls, style, category):
+        def color_from_style(color):
+            try:
+                ret = []
+                if color.startswith("rgb"):
+                    ret = color[color.find("(") + 1 : color.find(")")].split(",")
+                    ret = [float(x) for x in ret]
+                if color.startswith("#"):
+                    ret = [
+                        int(color[x : x + 2], 16) / 255 for x in range(1, len(color), 2)
+                    ]
+                if 2 < len(ret) < 5:
+                    return tuple(ret)
+            except:
+                pass
+
+        fallback = cls.get(category)
+        if style:
+            bg = color_from_style(style.get("background"))
+            color = color_from_style(style.get("color"))
+        else:
+            bg = None
+            color = None
+        if bg is None:
+            bg = fallback.bg
+        if color is None:
+            color = fallback.flowers[0].fg
+        flower_colors = [FlowerTheme(color)]
+        return cls(bg, flower_colors)
+
+    @classmethod
+    def get(cls, key):
+        if key in color_themes:
+            return color_themes[key]
+        else:
+            return default_color_theme
+
+
+color_themes = {
+    "Classic": ColorTheme((0.1, 0.4, 0.3), [FlowerTheme((1.0, 0.6, 0.4, 0.4), True)]),
+    "SolidBlack": ColorTheme((0, 0, 0), [FlowerTheme((0, 0, 0), True)]),
+    "Updates": ColorTheme((0.9, 0.8, 0), [FlowerTheme((0.9, 0.6, 0))]),
+    "Installed": ColorTheme(installed_col, [FlowerTheme((0, 0.8, 0.8))]),
+    "Music": ColorTheme((1, 0.4, 0.7), [FlowerTheme((1.0, 0.8, 0))]),
+    "Games": ColorTheme((0.9, 0.5, 0.1), [FlowerTheme((0.5, 0.3, 0.1))]),
+    "Badge": ColorTheme((0.3, 0.4, 0.7), [FlowerTheme((0, 0.8, 0.8))]),
+    "Apps": ColorTheme((0.7, 0, 0.7), [FlowerTheme((0, 0, 0))]),
+    "Demos": ColorTheme((0, 0, 0), [FlowerTheme((0.5, 0.5, 0.5))]),
+    "Media": ColorTheme((0, 0, 0), [FlowerTheme((0.5, 0.5, 0.5))]),
+    "All": ColorTheme((0.18, 0.81, 0.36), [FlowerTheme((0, 0.3, 0))]),
+    "Seeds": ColorTheme(
+        (0.1, 0.1, 0.1), [FlowerTheme([x * 0.7 for x in col]) for col in seed_cols]
+    ),
+    "Featured": ColorTheme(
+        (0.2, 0.0, 0.3),
+        [FlowerTheme((0.9, 0.6, 0)), FlowerTheme((1.0, 0.4, 0.7), True)],
+    ),
+}
+
+default_color_theme = color_themes["Classic"]
+
 
 class Flow3rView(BaseView):
-    def __init__(self) -> None:
+    def __init__(self, colors=None) -> None:
         super().__init__()
+        self.colors = default_color_theme if colors is None else colors
 
         self.flowers = []
-        for i in range(8):
-            self.flowers.append(
-                Flower(
-                    ((random.getrandbits(16) - 32767) / 32767.0) * 200,
-                    ((random.getrandbits(16)) / 65535.0) * 240 - 120,
-                    ((random.getrandbits(16)) / 65535.0) * 400 + 25,
-                )
+        for i in range(self.colors.num_stars):
+            flower_color = random.choice(self.colors.flowers)
+            flower = Flower(
+                ((random.getrandbits(16) - 32767) / 32767.0) * 200,
+                ((random.getrandbits(16)) / 65535.0) * 240 - 120,
+                ((random.getrandbits(16)) / 65535.0) * 400 + 25,
+                colors=flower_color,
             )
+            flower.fg_col_prev = flower.fg_col
+            flower.fg_col_target = flower.fg_col
+            self.flowers.append(flower)
+        self.bg_col = self.colors.bg
+        self.bg_col_prev = self.bg_col
+        self.bg_col_target = self.bg_col
+        self.color_blend = None
+
+    def update_colors(self, colors):
+        self.colors = colors
+        self.bg_col_prev = self.bg_col
+        self.bg_col_target = self.colors.bg
+        for f in self.flowers:
+            flower_color = random.choice(self.colors.flowers)
+            f.fg_col_prev = f.fg_col
+            f.fg_col_target = flower_color.fg
+        self.color_blend = 0
 
     def think(self, ins: InputState, delta_ms: int) -> None:
         super().think(ins, delta_ms)
-        for c in self.flowers:
-            c.y += (10 * delta_ms / 1000.0) * 200 / c.z
-            if c.y > 300:
-                c.y = -300
-            c.rot += float(delta_ms) * c.rot_speed
-        self.flowers = sorted(self.flowers, key=lambda c: -c.z)
+        for f in self.flowers:
+            f.y += (10 * delta_ms / 1000.0) * 200 / f.z
+            if f.y > 300:
+                f.y = -300
+            f.rot += float(delta_ms) * f.rot_speed
+        self.flowers = sorted(self.flowers, key=lambda f: -f.z)
+
+        if self.color_blend is not None:
+            self.color_blend += delta_ms / 160
 
     def draw(self, ctx: Context) -> None:
         ctx.save()
         ctx.rectangle(-120, -120, 240, 240)
-        ctx.rgb(0.1, 0.4, 0.3)
+        if self.color_blend is not None:
+            self.bg_col = color_blend(
+                self.bg_col_prev, self.bg_col_target, self.color_blend
+            )
+            for f in self.flowers:
+                f.fg_col = color_blend(f.fg_col_prev, f.fg_col_target, self.color_blend)
+            if self.color_blend >= 1:
+                self.color_blend = None
+
+        ctx.rgb(*self.bg_col)
         ctx.fill()
 
         for f in self.flowers:
@@ -40,7 +173,12 @@ class Flow3rView(BaseView):
 
 
 class Flower:
-    def __init__(self, x: float, y: float, z: float) -> None:
+    def __init__(self, x: float, y: float, z: float, colors=None) -> None:
+        self.fg_col = (1.0, 0.6, 0.4, 0.4)
+        self.solid = True
+        if colors is not None:
+            self.fg_col = colors.fg
+            self.solid = colors.solid
         self.x = x
         self.y = y
         self.z = z
@@ -49,12 +187,18 @@ class Flower:
 
     def draw(self, ctx: Context) -> None:
         ctx.save()
+        ctx.line_width = 4
         ctx.translate(-78 + self.x, -70 + self.y)
         ctx.translate(50, 40)
-
         ctx.rotate(self.rot)
         ctx.translate(-50, -40)
         ctx.scale(100 / self.z, 100.0 / self.z)
+
+        if len(self.fg_col) == 4:
+            ctx.rgba(*self.fg_col)
+        else:
+            ctx.rgb(*self.fg_col)
+
         ctx.move_to(76.221727, 3.9788409).curve_to(
             94.027758, 31.627675, 91.038918, 37.561293, 94.653428, 48.340473
         ).rel_curve_to(
@@ -75,32 +219,11 @@ class Flower:
             27.471602, 45.126773, 38.877997, 45.9184, 56.349456, 48.518302
         ).curve_to(
             59.03275, 31.351935, 64.893201, 16.103886, 76.221727, 3.9788409
-        ).close_path().rgba(
-            1.0, 0.6, 0.4, 0.4
-        ).fill()
-        ctx.restore()
-        return
-        ctx.move_to(116.89842, 17.221179).rel_curve_to(
-            6.77406, 15.003357, 9.99904, 35.088466, 0.27033, 47.639569
-        ).curve_to(
-            108.38621, 76.191194, 87.783414, 86.487988, 75.460015, 75.348373
-        ).curve_to(
-            64.051094, 64.686361, 61.318767, 54.582827, 67.499384, 36.894251
-        ).curve_to(
-            79.03955, 16.606134, 103.60918, 15.612261, 116.89842, 17.221179
-        ).close_path().rgb(
-            0.5, 0.3, 0.4
-        ).fill()
+        ).close_path()
+
+        if self.solid:
+            ctx.fill()
+        else:
+            ctx.stroke()
 
-        ctx.move_to(75.608612, 4.2453713).curve_to(
-            85.516707, 17.987709, 93.630911, 33.119248, 94.486497, 49.201225
-        ).curve_to(
-            95.068862, 60.147617, 85.880014, 75.820834, 74.919761, 75.632395
-        ).curve_to(
-            63.886159, 75.442695, 57.545631, 61.257211, 57.434286, 50.22254
-        ).curve_to(
-            57.257291, 32.681814, 65.992688, 16.610811, 75.608612, 4.2453713
-        ).close_path().rgb(
-            0.2, 0.5, 0.8
-        ).fill()
         ctx.restore()
diff --git a/python_payload/apps/gr33nhouse/confirmation.py b/python_payload/apps/gr33nhouse/confirmation.py
index 574fe85eb55c2cd32751c77dea0514dc5d70e188..acc221b45867eac932f41aeb7e8949de54ae4421 100644
--- a/python_payload/apps/gr33nhouse/confirmation.py
+++ b/python_payload/apps/gr33nhouse/confirmation.py
@@ -3,23 +3,185 @@ from st3m.ui import colours
 from st3m.ui.view import BaseView, ViewManager
 from ctx import Context
 from .background import Flow3rView
+from .background import broken_col, update_col, installed_col
 from .download import DownloadView
+from .delete import DeleteView
+from st3m.utils import wrap_text
+import math
+
+
+class Button:
+    def __init__(self, pos, size):
+        self.len = list(size)
+        self.mid = list(pos)
+        self.min = [pos[x] - size[x] / 2 for x in range(2)]
+        self.shift = 0
+
+        self.top_tags = list()
+        self.bot_tags = list()
+        self.text = "back"
+
+    def draw(self, ctx, highlight=False):
+        ctx.font_size = 18
+        ctx.gray(1)
+        if not highlight:
+            ctx.move_to(*self.mid)
+            ctx.text(self.text)
+            return
+
+        ctx.rectangle(*self.min, *self.len).fill()
+        ctx.fill()
+
+        ctx.save()
+        ctx.gray(0)
+        ctx.move_to(self.shift + self.mid[0], self.mid[1])
+        ctx.text_align = ctx.LEFT
+        ctx.text(self.text)
+        ctx.font_size = 14
+        ctx.rel_move_to(0, -6)
+        x_start = ctx.x
+        for text, col in self.top_tags:
+            ctx.rgb(*col)
+            ctx.text(" " + text)
+        x_stop = ctx.x
+        ctx.rel_move_to(x_start - x_stop, 12)
+        for text, col in self.bot_tags:
+            ctx.rgb(*col)
+            ctx.text(" " + text)
+        x_stop = max(x_stop, ctx.x)
+        ctx.restore()
+
+        if abs(x_asym := self.shift + x_stop) > 0.1:
+            self.shift -= x_asym / 2
+            self.draw(ctx, True)
+
+
+class InstallButton(Button):
+    def __init__(self, pos, size, version):
+        super().__init__(pos, size)
+
+        self.version = version
+        self.text = "install"
+        if version.patch:
+            self.text += " patch"
+        tags = self.top_tags
+        if version.broken:
+            tags.append(("broken", broken_col))
+            tags = self.bot_tags
+        if version.update:
+            tags.append(("update", update_col))
+            tags = self.bot_tags
+        if version.installed:
+            tags.append(("installed", installed_col))
+            tags = self.bot_tags
+
+
+class DeleteButton(Button):
+    def __init__(self, pos, size):
+        super().__init__(pos, size)
+        self.text = "delete"
+
+
+class ScrollBlock:
+    def __init__(self, raw_text):
+        self.num_lines = 2
+        self.line_height = 18
+        self.start = -87
+        self.width = 175
+        self.grad_len = 0
+        self.end = self.start + self.num_lines * self.line_height
+
+        self.raw_text = raw_text
+        self.speed = 0
+        self.pos = self.start + self.line_height / 2
+        self.grad = self.grad_len / (self.end - self.start + 2 * self.grad_len)
+        self.clip_min = self.start - self.grad_len
+        self.clip_max = self.end + self.grad_len
+
+    def get_lines(self, ctx):
+        self.lines = wrap_text(self.raw_text, self.width, ctx)
+        if not self.lines:
+            return
+
+        if len(self.lines) > self.num_lines:
+            self.speed = 1 / 160
+            self.lines.append("")
+        else:
+            self.pos += (self.num_lines - len(self.lines)) * self.line_height / 2
+
+        self.overflow_pos = len(self.lines) * self.line_height
+
+    def draw(self, ctx):
+        if not hasattr(self, "lines"):
+            self.get_lines(ctx)
+        if not self.lines:
+            return
+
+        ctx.save()
+        ctx.rectangle(
+            -self.width / 2, self.clip_min, self.width, self.clip_max - self.clip_min
+        ).clip()
+        if not self.speed:
+            for x, line in enumerate(self.lines):
+                pos = x * self.line_height + self.pos
+                ctx.move_to(0, pos)
+                ctx.text(line)
+        else:
+            self.pos %= self.overflow_pos
+            x = int(
+                (self.clip_min - self.line_height / 2 - self.pos) // self.line_height
+            )
+            while True:
+                line = self.lines[x % len(self.lines)]
+                pos = x * self.line_height + self.pos
+                if pos > self.clip_max + self.line_height / 2:
+                    break
+                if pos > self.end - self.line_height / 2:
+                    ctx.linear_gradient(0, self.clip_min, 0, self.clip_max)
+                    ctx.add_stop(0, (1.0, 1.0, 1.0), 1)
+                    ctx.add_stop(1 - self.grad, (1.0, 1.0, 1.0), 1)
+                    ctx.add_stop(1, (1.0, 1.0, 1.0), 0)
+                elif pos < self.start + self.line_height / 2:
+                    ctx.linear_gradient(0, self.clip_min, 0, self.clip_max)
+                    ctx.add_stop(0, (1.0, 1.0, 1.0), 0)
+                    ctx.add_stop(self.grad, (1.0, 1.0, 1.0), 1)
+                    ctx.add_stop(1, (1.0, 1.0, 1.0), 1)
+                else:
+                    ctx.gray(1)
+                ctx.move_to(0, pos)
+                ctx.text(line)
+                x += 1
+        ctx.restore()
+
+    def think(self, ins, delta_ms):
+        self.pos -= delta_ms * self.speed
 
 
 class ConfirmationView(BaseView):
     background: Flow3rView
 
-    url: str
-    name: str
-    author: str
-
-    def __init__(self, url: str, name: str, author: str) -> None:
+    def __init__(self, app) -> None:
         super().__init__()
-        self.background = Flow3rView()
-
-        self.url = url
-        self.name = name
-        self.author = author
+        self.background = Flow3rView(app.colors)
+
+        self.app = app
+
+    def on_enter(self, vm):
+        super().on_enter(vm)
+        self.buttons = []
+        self.button_index = 0
+        x = 0
+        size = [240, 25]
+        for version in self.app.available_versions:
+            pos = [0, 48 + 25 * x]
+            self.buttons.append(InstallButton(pos, size, version))
+            x += 1
+        if self.app.installed:
+            pos = [0, 48 + 25 * x]
+            self.buttons.append(DeleteButton(pos, size))
+            x += 1
+
+        self.desc = ScrollBlock(self.app.description)
 
     def on_exit(self) -> bool:
         # request thinks after on_exit
@@ -30,50 +192,61 @@ class ConfirmationView(BaseView):
 
         self.background.draw(ctx)
 
-        ctx.save()
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.MIDDLE
+
+        ctx.font_size = 17
+
+        self.desc.draw(ctx)
+
         ctx.rgb(*colours.WHITE)
         ctx.rectangle(
             -120.0,
-            -80.0,
+            -35.0,
             240.0,
-            160.0,
+            40.0,
         ).fill()
 
-        ctx.rgb(*colours.BLACK)
         ctx.font = "Camp Font 3"
-        ctx.text_align = ctx.CENTER
-        ctx.text_baseline = ctx.MIDDLE
-
-        ctx.font_size = 16
-        ctx.move_to(0, -60)
-        ctx.text("Install")
-
+        app = self.app
+        ctx.rgb(*colours.BLACK)
         ctx.font_size = 24
-        ctx.move_to(0, -30)
-        ctx.text(self.name)
-
-        if self.author:
-            ctx.font_size = 16
-            ctx.move_to(0, 0)
-            ctx.text("by")
+        ctx.move_to(0, -15)
+        ctx.text(app.name)
 
-            ctx.font_size = 24
-            ctx.move_to(0, 30)
-            ctx.text(self.author)
+        if app.author:
+            ctx.font_size = 17
+            ctx.gray(1)
+            ctx.move_to(0, 21)
+            ctx.text("by " + app.author)
 
-        ctx.font_size = 16
-        ctx.move_to(0, 60)
-        ctx.text("(OS shoulder to abort)")
-
-        ctx.restore()
+        for x, button in enumerate(self.buttons):
+            button.draw(ctx, highlight=x == self.button_index)
 
     def think(self, ins: InputState, delta_ms: int) -> None:
         super().think(ins, delta_ms)
         self.background.think(ins, delta_ms)
-
-        if self.is_active() and self.input.buttons.app.middle.pressed:
-            self.vm.replace(
-                DownloadView(
-                    url=self.url,
-                )
-            )
+        self.desc.think(ins, delta_ms)
+
+        if self.is_active():
+            self.button_index += self.input.buttons.app.right.pressed
+            self.button_index -= self.input.buttons.app.left.pressed
+            self.button_index %= len(self.buttons)
+            if self.input.buttons.app.middle.pressed:
+                button = self.buttons[self.button_index]
+                if isinstance(button, InstallButton):
+                    print("Installing", self.app.name, "from", button.version.tar_url)
+                    self.vm.push(
+                        DownloadView(
+                            self.app,
+                            button.version,
+                        )
+                    )
+                elif isinstance(button, DeleteButton):
+                    self.vm.push(
+                        DeleteView(
+                            self.app,
+                        )
+                    )
+                else:
+                    self.vm.pop()
diff --git a/python_payload/apps/gr33nhouse/delete.py b/python_payload/apps/gr33nhouse/delete.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b5348c6f0f7088d200bbdd95a6beb914ddf2896
--- /dev/null
+++ b/python_payload/apps/gr33nhouse/delete.py
@@ -0,0 +1,55 @@
+from st3m.input import InputState
+from st3m.goose import Optional, List
+from st3m.ui import colours
+from st3m.utils import sd_card_plugged
+from st3m.ui.view import BaseView
+from ctx import Context
+import os
+
+from st3m import application_settings
+
+
+class DeleteView(BaseView):
+    def __init__(self, app) -> None:
+        super().__init__()
+        self._app = app
+        self._delete = 0
+
+    def draw(self, ctx: Context) -> None:
+        ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
+
+        ctx.gray(1)
+        ctx.move_to(0, 0)
+        ctx.font = "Camp Font 3"
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.MIDDLE
+        ctx.font_size = 24
+        ctx.text(self._app.name)
+
+        ctx.move_to(0, -40)
+
+        ctx.font_size = 20
+        ctx.text("Delete app?")
+
+        for x in range(2):
+            x = 1 - x
+            ctx.translate(0, 35)
+            ctx.move_to(0, 0)
+            ctx.gray(1)
+            if x == self._delete:
+                ctx.rectangle(-120, -15, 240, 30).fill()
+                ctx.gray(0)
+            ctx.text("yes" if x else "no")
+
+    def think(self, ins: InputState, delta_ms: int) -> None:
+        super().think(ins, delta_ms)  # Let BaseView do its thing
+
+        self._delete += self.input.buttons.app.right.pressed
+        self._delete -= self.input.buttons.app.left.pressed
+        self._delete %= 2
+
+        if self.input.buttons.app.middle.pressed:
+            if self._delete:
+                application_settings.delete_app(self._app.installed_path)
+                self._app.update_installed_version(None)
+            self.vm.pop()
diff --git a/python_payload/apps/gr33nhouse/download.py b/python_payload/apps/gr33nhouse/download.py
index 1d06ac630977d87aefca73c630b0ccbbaa582585..c2711bbf4584bc66e6ab906c6abdc4aea57356bc 100644
--- a/python_payload/apps/gr33nhouse/download.py
+++ b/python_payload/apps/gr33nhouse/download.py
@@ -11,6 +11,7 @@ import gc
 import math
 from st3m.ui.view import BaseView
 from ctx import Context
+from st3m import application_settings
 
 
 class DownloadView(BaseView):
@@ -30,11 +31,13 @@ class DownloadView(BaseView):
     """
     _state: int
 
-    def __init__(self, url: str) -> None:
+    def __init__(self, app, version) -> None:
         super().__init__()
         self._state = 1
         self._try = 1
-        self._url = url
+        self._app = app
+        self._version = version
+        self._url = version.tar_url
         self.response = b""
         self._download_instance = None
         self.download_percentage = 0
@@ -197,6 +200,11 @@ class DownloadView(BaseView):
                 print(f"making {app_folder}")
                 os.mkdir(app_folder)
 
+            installed_path = app_folder + self._app.slug
+            if os.path.exists(installed_path):
+                print("removing old files at", installed_path)
+                application_settings.delete_app(installed_path)
+
             for i in t:
                 print(i.name)
 
@@ -216,4 +224,8 @@ class DownloadView(BaseView):
                         while data := f.read():
                             of.write(data)
 
+            self._app.installed_path = installed_path
+            self._app.update_installed_version(self._version)
+            print("installed at", installed_path)
+
             self._state = 5
diff --git a/python_payload/apps/gr33nhouse/manual.py b/python_payload/apps/gr33nhouse/manual.py
index 44e9c84f95a2418290a69cb6e0bfa9ffd828fd3d..0d1e05065a053365a10e92ef8a0b4aea83adac54 100644
--- a/python_payload/apps/gr33nhouse/manual.py
+++ b/python_payload/apps/gr33nhouse/manual.py
@@ -5,19 +5,13 @@ from st3m.ui.view import BaseView, ViewManager
 from ctx import Context
 from .confirmation import ConfirmationView
 from .background import Flow3rView
+from .background import seed_cols as PETAL_COLORS
 
 import math
 import urequests
 import time
 import gc
 
-PETAL_COLORS = [
-    (0, 0, 1),
-    (0, 1, 1),
-    (1, 1, 0),
-    (0, 1, 0),
-    (1, 0, 1),
-]
 PETAL_MAP = [0, 2, 4, 6, 8]
 ONE_FIFTH = math.pi * 2 / 5
 ONE_TENTH = math.pi * 2 / 10
@@ -33,9 +27,9 @@ class ManualInputView(BaseView):
     current_petal: Optional[int]
     wait_timer: Optional[int]
 
-    def __init__(self) -> None:
+    def __init__(self, colors=None) -> None:
         super().__init__()
-        self.background = Flow3rView()
+        self.background = Flow3rView(colors)
 
         self.flow3r_seed = ""
         self.current_petal = None
diff --git a/python_payload/st3m/application_settings.py b/python_payload/st3m/application_settings.py
index ed65cb6d4d4aa1a83d5b3374186389bbc4666287..2b478ee4eb15508228457780e8f16807d62d30e1 100644
--- a/python_payload/st3m/application_settings.py
+++ b/python_payload/st3m/application_settings.py
@@ -4,6 +4,7 @@ import os
 from st3m import logging
 from st3m import utils
 
+
 log = logging.Log(__name__, level=logging.INFO)
 
 # we are conciously only storing this on sd.
@@ -28,6 +29,8 @@ _application_data = None
 # currently used when application settings file doesn't exist.
 _application_settings_nothing_new = False
 
+_delete_callback = None
+
 
 def get_autostart():
     global _autostart_data
@@ -197,6 +200,29 @@ def create_app_data(slug):
     _save_application_data()
 
 
+def delete_app(path):
+    path = utils.simplify_path(path)
+    if path.startswith("/sd/apps/") or path.startswith("/flash/sys/apps/"):
+        if os.path.exists(path):
+            print(f"deleting {path}")
+            utils.rm_recursive(path)
+        else:
+            print(f"can't delete {path}, doesn't exist")
+    else:
+        print(f"can't delete {path}, not in appication folder")
+    slug = path.split("/")[-1]
+    if not slug in _application_data:
+        print(f"unknown slug: {slug}")
+    delete_app_data(slug)
+    if _delete_callback:
+        _delete_callback(slug)
+
+
+def set_delete_callback(fun):
+    global _delete_callback
+    _delete_callback = fun
+
+
 class ApplicationTag:
     def __init__(self, rating, text="tag", color=(1, 1, 1)):
         # rating affects sort order. at rating = 0
diff --git a/python_payload/st3m/main_menu.py b/python_payload/st3m/main_menu.py
index 23c1763ccefb81e9ed897b9d2483ce64238aa07f..3b0aaff87cefcaf5782c4054ac2de31662c43c4f 100644
--- a/python_payload/st3m/main_menu.py
+++ b/python_payload/st3m/main_menu.py
@@ -38,7 +38,7 @@ import bl00mbox
 
 
 class ApplicationTinyMenu(BaseView):
-    def __init__(self, item, item_destructor=None) -> None:
+    def __init__(self, item) -> None:
         super().__init__()
         self._vm = None
         self._index = 0
@@ -50,7 +50,6 @@ class ApplicationTinyMenu(BaseView):
         if not isinstance(item, MenuItemAppLaunch):
             self.exit()
         self._bundle = item._bundle
-        self._item_destructor = item_destructor
 
     def draw(self, ctx):
         phase = self._col_anim
@@ -148,14 +147,7 @@ class ApplicationTinyMenu(BaseView):
                 self._delete_confirm_index = 0
             elif self._delete_confirm_index == 2:
                 # delete
-                path = simplify_path(self._bundle.path)
-                if path.startswith("/sd/apps") or path.startswith("/flash/sys/apps"):
-                    print(f"deleting {path}...")
-                    rm_recursive(path)
-                else:
-                    print(f"can't delete {path}")
-                application_settings.delete_app_data(self._bundle.path)
-                self._item_destructor()
+                application_settings.delete_app(self._bundle.path)
                 self.exit()
             elif self._index == 0:
                 # change tag
@@ -259,6 +251,20 @@ class ApplicationMenu(RestoreMenu):
 
         return ret
 
+    def delete_app(self, slug):
+        pos = 0
+        for item in self._items:
+            target = self._scroll_controller.target_position()
+            if isinstance(item, MenuItemAppLaunch):
+                if item._bundle.path.endswith(slug):
+                    print(slug, item._bundle.path)
+                    self._items.pop(pos)
+                    if pos >= target:
+                        self._scroll_controller.set_position(pos - 1)
+                    pos -= 1
+                    self._scroll_controller.set_item_count(len(self._items))
+            pos += 1
+
     def _update_tags(self):
         for item in self._items:
             if isinstance(item, MenuItemAppLaunch):
@@ -268,12 +274,6 @@ class ApplicationMenu(RestoreMenu):
                         return
         self.tags = []
 
-    def _pop_target_item(self):
-        pos = self._scroll_controller.target_position()
-        self._items.pop(pos)
-        self._scroll_controller.set_position(pos - 1)
-        self._scroll_controller.set_item_count(len(self._items))
-
     def _sort(self):
         def sort_key(item):
             if isinstance(item, MenuItemBack):
@@ -299,7 +299,7 @@ class ApplicationMenu(RestoreMenu):
             if self.input.buttons.app.middle.repeated and self._vm:
                 item = self._items[self._scroll_controller.target_position()]
                 self._vm.push(
-                    ApplicationTinyMenu(item, self._pop_target_item),
+                    ApplicationTinyMenu(item),
                     ViewTransitionNone(),
                 )
             elif self.input.buttons.app.middle.released or any(
@@ -439,20 +439,28 @@ class MainMenu(SunMenu):
         for kind in app_kinds:
             if kind not in ["Hidden", "System"] and kind not in menu_categories:
                 menu_categories.append(kind)
-
-        categories = [
-            MenuItemApplicationMenu(
-                kind, ApplicationMenu([MenuItemApplicationBack()] + entries, name=kind)
-            )
+        self._app_menus = [
+            ApplicationMenu([MenuItemApplicationBack()] + entries, name=kind)
             for kind in menu_categories
             if (entries := _get_bundle_menu_entries(self._bundles, kind))
         ]
+
+        categories = [
+            MenuItemApplicationMenu(app_menu._name, app_menu)
+            for app_menu in self._app_menus
+        ]
         categories.append(MenuItemForeground("System", menu_system))
 
         self._items = categories
         # # self._scroll_controller = ScrollController()
         # self._scroll_controller.set_item_count(len(categories))
 
+        application_settings.set_delete_callback(self.delete_app)
+
+    def delete_app(self, slug):
+        for app_menu in self._app_menus:
+            app_menu.delete_app(slug)
+
     def on_enter(self, vm):
         super().on_enter(vm)
         self.sensitivity = 1