feat: MontaukOS installer app, toml config, app directory with manifests, and more

This commit is contained in:
2026-03-08 12:06:29 +01:00
parent 4d0177d55e
commit 1edbec3c66
60 changed files with 3045 additions and 268 deletions
@@ -1,12 +0,0 @@
/*
* app_devexplorer.cpp
* MontaukOS Desktop - Device Explorer launcher (spawns standalone devexplorer.elf)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_devexplorer(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/devexplorer.elf");
}
-11
View File
@@ -1,11 +0,0 @@
/*
* app_disks.cpp
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_disks(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/disks.elf");
}
-12
View File
@@ -1,12 +0,0 @@
/*
* app_doom.cpp
* MontaukOS Desktop - DOOM launcher (spawns standalone doom.elf)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_doom(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/games/doom.elf");
}
+24 -24
View File
@@ -92,7 +92,12 @@ static void filemanager_read_drives(FileManagerState* fm) {
fm->at_drives_root = true;
fm->current_path[0] = '\0';
for (int d = 0; d < FM_MAX_DRIVES; d++) {
int drives[FM_MAX_DRIVES];
int driveCount = montauk::drivelist(drives, FM_MAX_DRIVES);
for (int di = 0; di < driveCount; di++) {
int d = drives[di];
int i = fm->entry_count;
char probe[8];
if (d < 10) {
probe[0] = '0' + d;
@@ -106,21 +111,16 @@ static void filemanager_read_drives(FileManagerState* fm) {
probe[3] = '/';
probe[4] = '\0';
}
const char* tmp[1];
int r = montauk::readdir(probe, tmp, 1);
if (r >= 0) {
int i = fm->entry_count;
char label[64];
montauk::strcpy(label, "Drive ");
str_append(label, probe, 64);
montauk::strncpy(fm->entry_names[i], label, 63);
fm->entry_types[i] = 3; // drive
fm->entry_sizes[i] = 0;
fm->is_dir[i] = true;
fm->drive_indices[i] = d;
fm->entry_count++;
if (fm->entry_count >= 64) break;
}
char label[64];
montauk::strcpy(label, "Drive ");
str_append(label, probe, 64);
montauk::strncpy(fm->entry_names[i], label, 63);
fm->entry_types[i] = 3; // drive
fm->entry_sizes[i] = 0;
fm->is_dir[i] = true;
fm->drive_indices[i] = d;
fm->entry_count++;
if (fm->entry_count >= 64) break;
}
fm->selected = -1;
@@ -688,13 +688,13 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
}
str_append(fullpath, fm->entry_names[clicked_idx], 512);
if (is_image_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/imageviewer.elf", fullpath);
montauk::spawn("0:/apps/imageviewer/imageviewer.elf", fullpath);
} else if (is_font_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/fontpreview.elf", fullpath);
montauk::spawn("0:/apps/fontpreview/fontpreview.elf", fullpath);
} else if (is_pdf_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/pdfviewer.elf", fullpath);
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/spreadsheet.elf", fullpath);
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
} else if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
@@ -744,13 +744,13 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
}
str_append(fullpath, fm->entry_names[clicked_idx], 512);
if (is_image_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/imageviewer.elf", fullpath);
montauk::spawn("0:/apps/imageviewer/imageviewer.elf", fullpath);
} else if (is_font_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/fontpreview.elf", fullpath);
montauk::spawn("0:/apps/fontpreview/fontpreview.elf", fullpath);
} else if (is_pdf_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/pdfviewer.elf", fullpath);
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/os/spreadsheet.elf", fullpath);
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
} else if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
@@ -1,12 +0,0 @@
/*
* app_spreadsheet.cpp
* MontaukOS Desktop - Spreadsheet launcher (spawns standalone spreadsheet.elf)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_spreadsheet(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/spreadsheet.elf");
}
+3 -8
View File
@@ -17,12 +17,7 @@ static constexpr int TERM_TAB_GAP = 4;
static constexpr int TERM_PLUS_W = 28;
static constexpr int TERM_PLUS_PAD = 8;
// Compute the x-offset for left-aligned tabs within bar_w
static int term_tabs_origin(int bar_w, int tab_count) {
(void)bar_w;
(void)tab_count;
return 8;
}
static constexpr int TERM_TAB_PAD = 8;
struct TermTabState {
TerminalState* tabs[TERM_MAX_TABS];
@@ -150,7 +145,7 @@ static void terminal_on_draw(Window* win, Framebuffer& fb) {
c.fill_rect(0, 0, cr.w, TERM_TAB_BAR_H, bar_bg);
int fh = system_font_height();
int tab_x = term_tabs_origin(cr.w, tts->tab_count);
int tab_x = TERM_TAB_PAD;
for (int i = 0; i < tts->tab_count; i++) {
bool active = (i == tts->active_tab);
@@ -241,7 +236,7 @@ static void terminal_on_mouse(Window* win, MouseEvent& ev) {
if (ly >= TERM_TAB_BAR_H) return; // click in terminal area, ignore
int tab_x = term_tabs_origin(cr.w, tts->tab_count);
int tab_x = TERM_TAB_PAD;
for (int i = 0; i < tts->tab_count; i++) {
if (lx >= tab_x && lx < tab_x + TERM_TAB_W) {
// Check if click is on the close "x" (last 24px of tab)
-13
View File
@@ -1,13 +0,0 @@
/*
* app_weather.cpp
* MontaukOS Desktop - Weather app launcher
* Spawns weather.elf as a standalone Window Server process
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_weather(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/weather.elf");
}
-13
View File
@@ -1,13 +0,0 @@
/*
* app_wiki.cpp
* MontaukOS Desktop - Wikipedia launcher
* Spawns wikipedia.elf as a standalone Window Server process
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_wiki(DesktopState* ds) {
(void)ds;
montauk::spawn("0:/os/wikipedia.elf");
}
+1 -7
View File
@@ -156,7 +156,7 @@ inline void format_size(char* buf, int size) {
}
// ============================================================================
// Forward declarations for app launchers
// Forward declarations for app launchers (embedded apps only)
// ============================================================================
void open_terminal(DesktopState* ds);
@@ -166,16 +166,10 @@ void open_calculator(DesktopState* ds);
void open_texteditor(DesktopState* ds);
void open_texteditor_with_file(DesktopState* ds, const char* path);
void open_klog(DesktopState* ds);
void open_wiki(DesktopState* ds);
void open_weather(DesktopState* ds);
void open_procmgr(DesktopState* ds);
void open_mandelbrot(DesktopState* ds);
void open_devexplorer(DesktopState* ds);
void open_settings(DesktopState* ds);
void open_doom(DesktopState* ds);
void open_reboot_dialog(DesktopState* ds);
void open_wordprocessor(DesktopState* ds);
void open_spreadsheet(DesktopState* ds);
void open_shutdown_dialog(DesktopState* ds);
void open_disks(DesktopState* ds);
void desktop_poll_external_windows(DesktopState* ds);
+55 -30
View File
@@ -8,9 +8,10 @@
#include "apps/apps_common.hpp"
#include "wallpaper.hpp"
#include <montauk/toml.h>
// ============================================================================
// App Menu Data
// App Menu Data (dynamic — built at startup from embedded + external apps)
// ============================================================================
static constexpr int MENU_W = 220;
@@ -20,36 +21,16 @@ static constexpr int MENU_DIV_H = 10;
struct MenuRow {
bool is_category;
const char* label; // "" for divider-only rows
int app_id; // -1 for category headers / dividers
char label[48];
int app_id; // embedded dispatch ID, or -1 for categories/dividers
bool external; // true = spawn binary_path
char binary_path[128]; // full VFS path for external apps
SvgIcon* icon; // loaded menu icon (null for categories/dividers)
};
static constexpr int MENU_ROW_COUNT = 23;
static const MenuRow menu_rows[MENU_ROW_COUNT] = {
{ true, "Applications", -1 }, // cat 0
{ false, "Terminal", 0 },
{ false, "Files", 1 },
{ false, "Text Editor", 4 },
{ false, "Word Processor", 15 },
{ false, "Spreadsheet", 16 },
{ false, "Calculator", 3 },
{ true, "Internet", -1 }, // cat 1
{ false, "Wikipedia", 9 },
{ false, "Weather", 13 },
{ true, "System", -1 }, // cat 2
{ false, "System Info", 2 },
{ false, "Kernel Log", 5 },
{ false, "Processes", 6 },
{ false, "Devices", 8 },
{ false, "Disks", 17 },
{ true, "Games", -1 }, // cat 3
{ false, "Mandelbrot", 7 },
{ false, "DOOM", 10 },
{ true, "", -1 }, // divider (always visible)
{ false, "Settings", 11 },
{ false, "Reboot", 12 },
{ false, "Shutdown", 14 },
};
static constexpr int MAX_MENU_ROWS = 48;
inline MenuRow menu_rows[MAX_MENU_ROWS];
inline int menu_row_count = 0;
// Collapsible category state (categories 0-3 are toggleable; divider category always expanded)
static constexpr int MENU_NUM_CATS = 5;
@@ -77,12 +58,52 @@ inline int menu_row_height(const MenuRow& row) {
inline int menu_total_height() {
int h = 10; // top + bottom padding
for (int i = 0; i < MENU_ROW_COUNT; i++)
for (int i = 0; i < menu_row_count; i++)
if (menu_row_visible(i))
h += menu_row_height(menu_rows[i]);
return h;
}
// ============================================================================
// Menu Builder Helpers
// ============================================================================
inline int menu_add_category(const char* label) {
if (menu_row_count >= MAX_MENU_ROWS) return -1;
MenuRow& r = menu_rows[menu_row_count++];
r.is_category = true;
montauk::strncpy(r.label, label, sizeof(r.label));
r.app_id = -1;
r.external = false;
r.binary_path[0] = '\0';
r.icon = nullptr;
return menu_row_count - 1;
}
inline int menu_add_embedded(const char* label, int app_id, SvgIcon* icon) {
if (menu_row_count >= MAX_MENU_ROWS) return -1;
MenuRow& r = menu_rows[menu_row_count++];
r.is_category = false;
montauk::strncpy(r.label, label, sizeof(r.label));
r.app_id = app_id;
r.external = false;
r.binary_path[0] = '\0';
r.icon = icon;
return menu_row_count - 1;
}
inline int menu_add_external(const char* label, const char* binary, SvgIcon* icon) {
if (menu_row_count >= MAX_MENU_ROWS) return -1;
MenuRow& r = menu_rows[menu_row_count++];
r.is_category = false;
montauk::strncpy(r.label, label, sizeof(r.label));
r.app_id = -1;
r.external = true;
montauk::strncpy(r.binary_path, binary, sizeof(r.binary_path));
r.icon = icon;
return menu_row_count - 1;
}
// ============================================================================
// Forward Declarations (internal functions)
// ============================================================================
@@ -95,6 +116,10 @@ gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge);
void desktop_draw_app_menu(gui::DesktopState* ds);
void desktop_draw_net_popup(gui::DesktopState* ds);
// main.cpp
void desktop_scan_apps(gui::DesktopState* ds);
void desktop_build_menu(gui::DesktopState* ds);
// Month names (shared by panel clock)
inline const char* month_names[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+20 -24
View File
@@ -202,7 +202,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
// Walk visible rows to find which one was clicked
int iy = menu_y + 5;
int cur_cat = -1;
for (int i = 0; i < MENU_ROW_COUNT; i++) {
for (int i = 0; i < menu_row_count; i++) {
const MenuRow& row = menu_rows[i];
if (row.is_category) cur_cat++;
if (!menu_row_visible(i)) continue;
@@ -212,25 +212,25 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
// Toggle category expand/collapse
menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat];
} else if (!row.is_category) {
switch (row.app_id) {
case 0: open_terminal(ds); break;
case 1: open_filemanager(ds); break;
case 2: open_sysinfo(ds); break;
case 3: open_calculator(ds); break;
case 4: open_texteditor(ds); break;
case 5: open_klog(ds); break;
case 6: open_procmgr(ds); break;
case 7: open_mandelbrot(ds); break;
case 8: open_devexplorer(ds); break;
case 9: open_wiki(ds); break;
case 10: open_doom(ds); break;
case 11: open_settings(ds); break;
case 12: open_reboot_dialog(ds); break;
case 13: open_weather(ds); break;
case 14: open_shutdown_dialog(ds); break;
case 15: open_wordprocessor(ds); break;
case 16: open_spreadsheet(ds); break;
case 17: open_disks(ds); break;
if (row.external) {
// Launch external app from manifest
montauk::spawn(row.binary_path);
} else {
// Dispatch embedded app
switch (row.app_id) {
case 0: open_terminal(ds); break;
case 1: open_filemanager(ds); break;
case 2: open_sysinfo(ds); break;
case 3: open_calculator(ds); break;
case 4: open_texteditor(ds); break;
case 5: open_klog(ds); break;
case 6: open_procmgr(ds); break;
case 7: open_mandelbrot(ds); break;
case 11: open_settings(ds); break;
case 12: open_reboot_dialog(ds); break;
case 14: open_shutdown_dialog(ds); break;
case 15: open_wordprocessor(ds); break;
}
}
ds->app_menu_open = false;
}
@@ -534,10 +534,6 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key
open_klog(ds);
return;
}
if (key.ascii == 'd' || key.ascii == 'D') {
open_doom(ds);
return;
}
}
// Dispatch to focused window
+176 -7
View File
@@ -6,6 +6,173 @@
#include "desktop_internal.hpp"
// ============================================================================
// App Manifest Scanning
// ============================================================================
// Extract the basename from a readdir entry.
// readdir returns full paths from drive root (e.g. "apps/doom/") —
// strip the directory prefix and any trailing slash to get just "doom".
static void extract_basename(char* out, int outSz, const char* entry) {
// Strip trailing slash
int len = montauk::slen(entry);
while (len > 0 && entry[len - 1] == '/') len--;
// Find last slash before that
int last_slash = -1;
for (int i = 0; i < len; i++) {
if (entry[i] == '/') last_slash = i;
}
const char* base = (last_slash >= 0) ? entry + last_slash + 1 : entry;
int base_len = len - (last_slash >= 0 ? last_slash + 1 : 0);
if (base_len >= outSz) base_len = outSz - 1;
montauk::memcpy(out, base, base_len);
out[base_len] = '\0';
}
void desktop_scan_apps(DesktopState* ds) {
ds->external_app_count = 0;
// Ensure the apps directory exists
montauk::fmkdir("0:/apps");
const char* entries[32];
int count = montauk::readdir("0:/apps", entries, 32);
for (int i = 0; i < count && ds->external_app_count < MAX_EXTERNAL_APPS; i++) {
// readdir returns paths like "apps/doom/" — extract just "doom"
char dirname[64];
extract_basename(dirname, sizeof(dirname), entries[i]);
if (dirname[0] == '\0') continue;
// Try to open the manifest in this app directory
char manifest_path[128];
snprintf(manifest_path, sizeof(manifest_path), "0:/apps/%s/manifest.toml", dirname);
int fh = montauk::open(manifest_path);
if (fh < 0) continue;
uint64_t sz = montauk::getsize(fh);
if (sz == 0 || sz > 4096) { montauk::close(fh); continue; }
char* text = (char*)montauk::malloc(sz + 1);
montauk::read(fh, (uint8_t*)text, 0, sz);
montauk::close(fh);
text[sz] = '\0';
auto doc = montauk::toml::parse(text);
montauk::mfree(text);
ExternalApp* app = &ds->external_apps[ds->external_app_count];
// Read manifest fields
const char* name = doc.get_string("app.name", "Unknown");
const char* binary = doc.get_string("app.binary", "");
const char* icon_file = doc.get_string("app.icon", "");
const char* category = doc.get_string("menu.category", "Applications");
bool visible = doc.get_bool("menu.visible", true);
montauk::strncpy(app->name, name, sizeof(app->name));
montauk::strncpy(app->category, category, sizeof(app->category));
app->menu_visible = visible;
// Build full binary path: 0:/apps/<dir>/<binary>
snprintf(app->binary_path, sizeof(app->binary_path),
"0:/apps/%s/%s", dirname, binary);
// Load icon from app directory
app->icon = {};
if (icon_file[0]) {
char icon_path[128];
snprintf(icon_path, sizeof(icon_path),
"0:/apps/%s/%s", dirname, icon_file);
Color defColor = colors::ICON_COLOR;
app->icon = svg_load(icon_path, 20, 20, defColor);
}
doc.destroy();
ds->external_app_count++;
}
}
// ============================================================================
// Menu Builder
// ============================================================================
// Category definitions and their ordering
static const char* CATEGORY_NAMES[] = { "Applications", "Internet", "System", "Games" };
static constexpr int NUM_CATEGORIES = 4;
// Embedded app definitions: { display_name, app_id, category_index }
struct EmbeddedAppDef {
const char* name;
int app_id;
int category; // index into CATEGORY_NAMES
};
static const EmbeddedAppDef embedded_apps[] = {
{ "Terminal", 0, 0 },
{ "Files", 1, 0 },
{ "Text Editor", 4, 0 },
{ "Word Processor", 15, 0 },
{ "Calculator", 3, 0 },
{ "System Info", 2, 2 },
{ "Kernel Log", 5, 2 },
{ "Processes", 6, 2 },
{ "Mandelbrot", 7, 3 },
};
static constexpr int NUM_EMBEDDED = sizeof(embedded_apps) / sizeof(embedded_apps[0]);
// Resolve embedded app_id to an icon pointer in DesktopState
static SvgIcon* icon_for_embedded(DesktopState* ds, int app_id) {
switch (app_id) {
case 0: return &ds->icon_terminal;
case 1: return &ds->icon_filemanager;
case 2: return &ds->icon_sysinfo;
case 3: return &ds->icon_calculator;
case 4: return &ds->icon_texteditor;
case 5: return &ds->icon_terminal; // Kernel Log uses terminal icon
case 6: return &ds->icon_procmgr;
case 7: return &ds->icon_mandelbrot;
case 15: return &ds->icon_texteditor; // Word Processor uses text editor icon
default: return nullptr;
}
}
void desktop_build_menu(DesktopState* ds) {
menu_row_count = 0;
// Build each category
for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
menu_add_category(CATEGORY_NAMES[cat]);
// Add embedded apps in this category
for (int e = 0; e < NUM_EMBEDDED; e++) {
if (embedded_apps[e].category == cat) {
menu_add_embedded(embedded_apps[e].name, embedded_apps[e].app_id,
icon_for_embedded(ds, embedded_apps[e].app_id));
}
}
// Add external apps in this category
for (int x = 0; x < ds->external_app_count; x++) {
ExternalApp* app = &ds->external_apps[x];
if (!app->menu_visible) continue;
if (montauk::streq(app->category, CATEGORY_NAMES[cat])) {
menu_add_external(app->name, app->binary_path, &app->icon);
}
}
}
// Divider + always-visible entries
menu_add_category(""); // divider (cat 4, always expanded)
menu_add_embedded("Settings", 11, &ds->icon_settings);
menu_add_embedded("Reboot", 12, &ds->icon_reboot);
menu_add_embedded("Shutdown", 14, &ds->icon_shutdown);
}
// ============================================================================
// Initialization
// ============================================================================
@@ -58,14 +225,12 @@ void gui::desktop_init(DesktopState* ds) {
ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor);
ds->icon_shutdown = svg_load("0:/icons/system-shutdown.svg", 20, 20, defColor);
ds->icon_weather = svg_load("0:/icons/weather-widget.svg", 20, 20, defColor);
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
ds->icon_doom = svg_load("0:/icons/doom.svg", 20, 20, defColor);
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
ds->icon_devexplorer = svg_load("0:/icons/hardware.svg", 20, 20, defColor);
ds->icon_disks = svg_load("0:/icons/gparted.svg", 20, 20, defColor); // gparted icon, i.e. disk/partition management
ds->icon_spreadsheet = svg_load("0:/icons/spreadsheet.svg", 20, 20, defColor);
// Scan 0:/apps/ for external app manifests and build the menu
desktop_scan_apps(ds);
desktop_build_menu(ds);
// Settings defaults
ds->settings.bg_gradient = true;
@@ -236,6 +401,10 @@ void gui::desktop_run(DesktopState* ds) {
// Handle mouse events
desktop_handle_mouse(ds);
// Re-poll external windows so that any killed during mouse/key
// handling are removed before we touch their pixel buffers.
desktop_poll_external_windows(ds);
// Compose and present
desktop_compose(ds);
ds->fb.flip();
+3 -28
View File
@@ -151,34 +151,12 @@ void desktop_draw_app_menu(DesktopState* ds) {
fill_rounded_rect(fb, menu_x, menu_y, MENU_W, menu_h, 8, colors::MENU_BG);
draw_rect(fb, menu_x, menu_y, MENU_W, menu_h, colors::BORDER);
// Icon lookup by app_id
SvgIcon* icons[18] = {
&ds->icon_terminal, // 0
&ds->icon_filemanager, // 1
&ds->icon_sysinfo, // 2
&ds->icon_calculator, // 3
&ds->icon_texteditor, // 4
&ds->icon_terminal, // 5 (Kernel Log)
&ds->icon_procmgr, // 6
&ds->icon_mandelbrot, // 7
&ds->icon_devexplorer, // 8
&ds->icon_wikipedia, // 9
&ds->icon_doom, // 10
&ds->icon_settings, // 11
&ds->icon_reboot, // 12
&ds->icon_weather, // 13
&ds->icon_shutdown, // 14
&ds->icon_texteditor, // 15
&ds->icon_spreadsheet, // 16
&ds->icon_disks, // 17 (Disks)
};
int mx = ds->mouse.x;
int my = ds->mouse.y;
int iy = menu_y + 5;
int cur_cat = -1;
for (int i = 0; i < MENU_ROW_COUNT; i++) {
for (int i = 0; i < menu_row_count; i++) {
const MenuRow& row = menu_rows[i];
if (row.is_category) cur_cat++;
@@ -239,11 +217,8 @@ void desktop_draw_app_menu(DesktopState* ds) {
// Icon
int icon_x = item_rect.x + 8;
int icon_y = item_rect.y + (row_h - 20) / 2;
if (row.app_id >= 0 && row.app_id < 18) {
SvgIcon* icon = icons[row.app_id];
if (icon && icon->pixels) {
fb.blit_alpha(icon_x, icon_y, icon->width, icon->height, icon->pixels);
}
if (row.icon && row.icon->pixels) {
fb.blit_alpha(icon_x, icon_y, row.icon->width, row.icon->height, row.icon->pixels);
}
// Label