Launcher: a grid of icons, with badges; the list stays as a Setting

The Launcher shows the Apps as a 4 x 3 grid of 32 x 32 icons, the
selected one's name under it (issue #9). The icons are the website's
own, doubled, and two drawn the same way for SSH and the Shell; they
are PNGs in assets/icons, turned into arrays by scripts/make_icons.py,
which CI checks. The selected tile is the website's: dark blue on cyan,
with notched corners.

- GridModel (lib/ui, host-tested) holds the selection and the scrolling.
- App::badge(): a dot on the tile for unread IRC messages, a Track
  recording, a Capture running, an SSH session open.
- Settings > Launcher: Grid or List.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-09 23:14:37 +02:00
co-authored by Claude Opus 5.5
parent aadfd8cb37
commit e2326e113b
40 changed files with 907 additions and 24 deletions
@@ -46,6 +46,15 @@ void test_toggle_rows_flip_on_select() {
TEST_ASSERT_FALSE(f.settings.getBool(Setting::Sound));
}
void test_the_launcher_is_a_grid_or_a_list() {
Fixture f;
int launcher = f.row(SettingsMenu::Row::Launcher);
TEST_ASSERT_EQUAL_STRING("Grid", f.menu.value(launcher).c_str());
f.menu.toggle(launcher);
TEST_ASSERT_TRUE(f.settings.getBool(Setting::LauncherList));
TEST_ASSERT_EQUAL_STRING("List", f.menu.value(launcher).c_str());
}
void test_choice_rows_list_options_and_mark_the_current_one() {
Fixture f;
int tz = f.row(SettingsMenu::Row::Timezone);
@@ -161,6 +170,7 @@ int main() {
RUN_TEST(test_every_row_has_a_label);
RUN_TEST(test_values_are_human_readable);
RUN_TEST(test_toggle_rows_flip_on_select);
RUN_TEST(test_the_launcher_is_a_grid_or_a_list);
RUN_TEST(test_choice_rows_list_options_and_mark_the_current_one);
RUN_TEST(test_choosing_applies_the_setting);
RUN_TEST(test_choice_that_breaks_a_rule_is_refused_with_a_reason);