diff --git a/python_payload/apps/gr33nhouse/applist.py b/python_payload/apps/gr33nhouse/applist.py
index c89b3eb86b9897f251fcbf4d7ad7b00fabea45fe..40b2739b9e065c25ed8a1b8cbf4507db552b0595 100644
--- a/python_payload/apps/gr33nhouse/applist.py
+++ b/python_payload/apps/gr33nhouse/applist.py
@@ -18,18 +18,19 @@ class ViewState(Enum):
     LOADED = 4
 
 
-class AppList(BaseView):
+class AppSubList(BaseView):
     _scroll_pos: float = 0.0
-    _state: ViewState = ViewState.INITIAL
 
     apps: list[Any] = []
 
     background: Flow3rView
 
-    def __init__(self) -> None:
+    def __init__(self, apps) -> None:
         super().__init__()
         self.background = Flow3rView()
         self._sc = ScrollController()
+        self.apps = apps
+        self._sc.set_item_count(len(self.apps))
 
     def on_exit(self) -> bool:
         # request thinks after on_exit
@@ -38,6 +39,98 @@ class AppList(BaseView):
     def draw(self, ctx: Context) -> None:
         ctx.move_to(0, 0)
 
+        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
+
+        ctx.move_to(0, 0)
+        for idx, app in enumerate(self.apps):
+            target = idx == self._sc.target_position()
+            if target:
+                ctx.gray(0.0)
+            else:
+                ctx.gray(1.0)
+
+            if abs(self._sc.current_position() - idx) <= 5:
+                xpos = 0.0
+                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"])
+            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)
+        self._scroll_pos += delta_ms / 1000
+
+        if not self.is_active():
+            return
+
+        if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
+            self._sc.scroll_left()
+            self._scroll_pos = 0.0
+        elif (
+            self.input.buttons.app.right.pressed
+            or self.input.buttons.app.right.repeated
+        ):
+            self._sc.scroll_right()
+            self._scroll_pos = 0.0
+        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,
+                )
+            )
+
+
+class AppList(BaseView):
+    _scroll_pos: float = 0.0
+    _state: ViewState = ViewState.INITIAL
+
+    items: list[Any] = ["All"]
+    category_order: list[Any] = ["Badge", "Music", "Media", "Apps", "Games"]
+
+    background: Flow3rView
+
+    def __init__(self) -> None:
+        super().__init__()
+        self.background = Flow3rView()
+        self._sc = ScrollController()
+        self._sc.set_item_count(len(self.items))
+
+    def draw(self, ctx):
+        ctx.move_to(0, 0)
+
         if self._state == ViewState.INITIAL or self._state == ViewState.LOADING:
             ctx.rgb(*colours.BLACK)
             ctx.rectangle(
@@ -78,6 +171,8 @@ class AppList(BaseView):
             return
 
         elif self._state == ViewState.LOADED:
+            ctx.move_to(0, 0)
+
             self.background.draw(ctx)
 
             ctx.save()
@@ -90,7 +185,6 @@ class AppList(BaseView):
             ).fill()
 
             ctx.translate(0, -30 * self._sc.current_position())
-
             offset = 0
 
             ctx.font = "Camp Font 3"
@@ -99,7 +193,7 @@ class AppList(BaseView):
             ctx.text_baseline = ctx.MIDDLE
 
             ctx.move_to(0, 0)
-            for idx, app in enumerate(self.apps):
+            for idx, item in enumerate(self.items):
                 target = idx == self._sc.target_position()
                 if target:
                     ctx.gray(0.0)
@@ -108,10 +202,10 @@ class AppList(BaseView):
 
                 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(item)) > 220:
                         xpos = sin(self._scroll_pos) * (width - 220) / 2
                     ctx.move_to(xpos, offset)
-                    ctx.text(app["name"])
+                    ctx.text(item)
                 offset += 30
 
             ctx.restore()
@@ -136,8 +230,21 @@ class AppList(BaseView):
                     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))
+
+                def sortkey(obj):
+                    try:
+                        return self.category_order.index(obj)
+                    except ValueError:
+                        return len(self.category_order)
+
+                categories.sort(key=sortkey)
+                self.items = ["All"] + categories
+                self._sc.set_item_count(len(self.items))
+
                 self._state = ViewState.LOADED
-                self._sc.set_item_count(len(self.apps))
                 print("App list loaded")
             except Exception as e:
                 print(f"Load failed: {e}")
@@ -166,15 +273,9 @@ class AppList(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._sc.target_position():
+                category = self.items[self._sc.target_position()]
+                apps = [app for app in self.apps if app.get("menu") == category]
+            else:
+                apps = list(self.apps)
+            self.vm.push(AppSubList(apps=apps))
diff --git a/python_payload/apps/gr33nhouse/confirmation.py b/python_payload/apps/gr33nhouse/confirmation.py
index 68328576533c97f3418149d98b46d6833ba6eea9..574fe85eb55c2cd32751c77dea0514dc5d70e188 100644
--- a/python_payload/apps/gr33nhouse/confirmation.py
+++ b/python_payload/apps/gr33nhouse/confirmation.py
@@ -52,13 +52,14 @@ class ConfirmationView(BaseView):
         ctx.move_to(0, -30)
         ctx.text(self.name)
 
-        ctx.font_size = 16
-        ctx.move_to(0, 0)
-        ctx.text("by")
-
-        ctx.font_size = 24
-        ctx.move_to(0, 30)
-        ctx.text(self.author)
+        if self.author:
+            ctx.font_size = 16
+            ctx.move_to(0, 0)
+            ctx.text("by")
+
+            ctx.font_size = 24
+            ctx.move_to(0, 30)
+            ctx.text(self.author)
 
         ctx.font_size = 16
         ctx.move_to(0, 60)