kodeOS internals
Launcher
Apps, the rail of folders and apps: how a row is chosen and launched, the details and Remove, where the rows come from, and what the empty launcher shows.
What it is
The launcher is the screen that lists the apps. The interface calls it Apps. It is one rail of rows on a black page: every row is an icon disc and a name, the focused row stands on a fixed line in the middle of the glass, and the rows pass under it. A yellow ring marks the icon in focus.
The rail has two levels. The root lists the folders and then the apps that belong to none. A folder lists its apps. The apps are the installed ones first, in the order of the apps region, and then the .bin files on a storage medium that are not installed.
The row in focus is the one a button acts on:
- TOP released goes in on it. A folder opens, an installed app launches, a file is written and then launched. There is no card between a row and a launch.
- TOP held, or a finger held on the row for 0.6 s, opens the details of an app: its icon, version, size and build date, and a red Remove pill for an installed app.
- BOT released, the pad's left or a swipe right goes back: out of a folder, and from the root to the face.
With no apps at all the launcher shows one box instead: "No apps yet" with Kody wondering under a question mark, and a QR code to apps.kode.diy one tap away.
The list does not show itself half dressed. It stays off the glass until the look of every app (colour, name and icon) is read, or until 1,500 ms (KGUI_META_WAIT_MS) after the listings end, and then it fades in whole.
How it is connected
flowchart LR appmgr["AO_AppMgr<br/>installed apps"] -- "APP_ENTRY_SIG, pages of 24" --> gui["AO_Gui<br/>rows[]"] storage["AO_Storage<br/>.bin files, manifests, app data"] -- "STORAGE_ENTRY_SIG" --> gui gui -- "kgui_list, kgui_motion<br/>kgui_group" --> rail["app_list<br/>the rail"] gui -- "TOP on an app row" --> run["appmgr_run_request<br/>appmgr_flash_request"] class appmgr ao class storage ao class gui ao
The models
Gui_browsing in kgui_ao.c shows the screen, which kgui_screen_launcher.c builds as a window on the whole glass with the empty box behind it. kgui_list.c makes the box that the rows move in, the focus ring and the one row every row is. kgui_motion.c is the model of the rail with no LVGL in it. kgui_group.c files the apps into folders, also with no LVGL and no QP.
AO_Gui keeps two models, installed[] and entries[], each up to 64 entries, in PSRAM. One rows[] array says for each row of the level which kind it is (a folder, an installed app or a file) and which index it stands for. pills[] holds the LVGL objects with the same indexes.
The rail
The rail is a window with a box in it. The rows and the ring are children of the box. It has one number, list_u, the selection coordinate in pixels: row i is at rest when u is i * KGUI_ROW_PITCH. A motion changes that number, gui_list_place() writes the box and the ring from it, and LVGL moves every row with the box. LVGL scrolls nothing of the rail.
Every row is the same size and has no ground of its own. It is an 80 px disc and a name. Nothing along a motion changes a row's size, ground or text; only the name's colour and line count follow the focus.
- An app's disc is the app's own 64 px picture drawn at the disc's size, when its data folder gave one. Otherwise it is the app's colour, or Medium Grey when it gave no colour, with the first two letters of its name in capitals.
- A folder's disc wears the folder's standard glyph on its colour (the table under The folders). Every other name wears the apps glyph on Medium Grey. The glyph is Soft Black on all of them.
- The name is Light Grey, up to two lines, while focused. Otherwise it is Medium Grey on one line, cut with dots.
The focus line is fixed at y 251 (KGUI_RAIL_FOCUS_Y). A list of three rows or fewer (KGUI_RAIL_BLOCK_MAX) does not scroll: it is a block centred on the focus line and the focus walks its rows. Rows whose icon starts above y 99 or ends below y 403 are pushed in along the rounded corner and fade toward the edge, and their names narrow by the push. A tap on a row whose icon is not wholly inside the rail only focuses it.
The ring
The focus is one ring, an object that stands on the focused icon: a circle the size of the disc with the kit's yellow focus ring around it, 4 px wide and 4 px outside the disc. It shows whatever the input, so a list moved by a finger keeps its mark of the row TOP opens.
The selection
The selection is the row nearest the focus line. sel_idx and sel_name hold it, and gui_select() is the only function that moves it. A row's identity is the project name of an installed app, the file name of a file, and /<folder> for a folder. gui_info prints it as sel=.
After every repaint, gui_resync_selection() finds the name again, or falls back to the first row. The name of the last launched row is stored in kgui/last and read at boot, so the cursor is where you left it.
The motions
The rail has four motions, all in kgui_motion.h as named defines.
| Motion | What it does |
|---|---|
| Step | The pad's up and down, a held pad's repeat, a tap on a half-hidden row. A critically damped spring of 200 ms (KGUI_STEP_PERIOD_MS) carries u. A held pad waits 400 ms, then steps every 115 ms; after 8 repeats a repeat steps 2 rows. No wrap |
| Glide | The lift of a finger carries the finger's speed 124 ms ahead. The list lands on the nearest row, at most 5 rows from where the finger lifted (KGUI_GLIDE_MAX_ROWS), on a spring of 280 ms and a damping of 0.9 |
| Rubber | A pull past the first or last row shows a fraction of itself (about half of a short pull, never more than 72 px) and comes back on a 300 ms spring. The pad meeting an end gives a short version of it. The end gives a thud |
| Slide | A change of level: the rows go out sideways, the rail is rebuilt, the new rows come in. 90 ms each way, 180 ms in all (KGUI_SLIDE_PAGE_MS). Into a folder the rows go left and the new ones come in from the right |
A press that lands on a list that moves stops it where it stands, and the click of that press acts on nothing. The finger moves u pixel for pixel.
A repaint keeps the motion. repaint_list() compares the rows of the level with the rows the list has, by identity. The same rows in the same order are drawn again where they stand; other rows are thrown away and made again, and a spring in flight stops where it stands.
The list appears when its metadata is read
The look of an app comes from its data folder, asked one at a time after a listing ends. A list that showed itself at once would show every row in its default look and then change it row by row. So kgui_meta.c holds the list back.
A row is settled when its folder has answered. The list appears when every row has settled, or 1,500 ms after the last listing ended, whichever is first. The tick checks the bound whether or not an event arrives, so a folder that never answers cannot keep the list off the glass. All the rows then appear at once and the list fades in over 150 ms (KGUI_LIST_REVEAL_MS). With no apps at all, the list opens at once.
From then on the list never hides. A new row after a rescan waits like the first ones did.
The Settings pull
A drag down on Apps pulls the settings screen down over it, and closing Settings goes back to Apps. A drag down that starts in the top 44 rows of the glass (KGUI_LIST_PULL_BAND) always pulls Settings, wherever the list stands. Lower down, a drag down is the pull only when all of these hold at the press (gui_zone_lock_pull()):
- The press landed on the list, and the list has rows.
- The first row is the selection, and the list rests there with no Slide running.
- Nothing has moved the list for 500 ms (
KGUI_LIST_STILL_MS).
If one fails, the drag moves the list. No drag on Apps locks the board: the face comes back by the way back from the root, or by the auto-lock.
The folders
An app belongs to the folder named by the first item of its category list, lowercased. The marketplace's closed list of categories names seven folders, by a table in kgui_group.c and never by a rule. Each word is taken in either of two forms, so game and games are the folder games, and ai is the folder AI. Any other word is a folder of its own, written as it is with its first letter in capitals.
- An app whose first category is empty or
other, and a.binthat has no manifest, belongs to none. It is a loose row at the root, under the folders. - Folders come in the table's order, then the other words alphabetically. At most 10 (
KGUI_GROUP_MAX), and the cap keeps the first folders of that order. The apps of a folder past the cap are loose rows. A folder of one app is still a folder. - A scanned
.binthat is not installed sits where its category puts it.
repaint_list() derives the level from whatever installed[] and entries[] carry on every pass, so an empty folder cannot exist. The root keeps its own selection while a folder is open, so the way back lands on the folder that was opened. When a rescan empties the open folder, the launcher falls back to the root. gui_info says level=root or level=group and group="<name>" or -. The code says "group" because "folder" already means an app's data folder.
The seven folders of the table have a colour and a glyph of their own, from the kit's palette. Yellow is the focus ring's and no folder's:
| Folder | Also taken as | Colour | Glyph |
|---|---|---|---|
| Games | game | Kody Blue (0x028AC2) | A gamepad |
| Tools | tool | Medium Grey (0xC2BAB3) | A wrench |
| Communication | communications | Kody Blue | A speech bubble |
| Wireless | wirelesses | OK green (0x31C45A) | Wi-Fi |
| Creative | creatives | Danger red (0xC9303D) | A pencil |
| Maker | makers | Medium Grey | A chip with its core |
| AI | ais | OK green | Two sparkles |
| Any other | Medium Grey | Apps |
From a row to an app
Going in
gui_level_enter() is the one function that goes in. TOP released, the pad's right and a tap on a row all end in it:
- On a folder row it opens the folder and Slides into it, keeping the root's selection.
- On an app row it is a launch.
- On an empty list it sends Kody away for the QR, or brings him back.
A tap on a row that is not the selection selects it and goes in on it in one gesture. Each way logs which input asked: group: games (top released after 215 ms) or launch: led (top released after 211 ms).
gui_back() is the way out. BOT released, the pad's left and a swipe right close the open folder and go to the root, and at the root they go to the face. HOME (BOT held 0.6 s) closes an open folder with no Slide and leaves Apps at its root.
Launching
gui_launch_selection() is the one verb. gui_level_enter() calls it on an app row, and Run again on the Stopped card calls it too.
- An installed row flies its icon to the middle of the glass, turns a Ring, fades to black and boots the app with
appmgr_run_request()(internals/gui/sheets). - A file row installs the file. AO_AppMgr is asked first which apps the write would evict. The write starts once the file fits: the oldest apps make room for it with no question, and AO_AppMgr copies each one it pushes out to the SD card first. The app boots once the write is done and the edge has closed.
The selected row is what runs. gui_launch_selection() refuses with a log line when an install is being decided already, when the list is empty, or when the selection no longer means what it meant when the press was made (launch refused: the list moved under the selection). The code rebuilds rows[] whenever either list answers, so the third check stops a press already on its way from landing on whichever row inherited the index.
The details
TOP held on an app row, or a finger held on it for 600 ms (GUI_LONG_PRESS_MS), opens the details over the launcher. A folder's row has none. The release that ends a hold launches nothing.
The details are one page on the sheet's card, in kshell's look:
- A 72 px disc (
KGUI_DETAILS_DISC_PX) with the app's icon, or its colour with the first two letters of its name in capitals, as on the rail. - The name, in the title face.
- The line
v<version> · <Category>in the caption face, muted, the category written as its folder is. - The app's own words, up to three lines. What does not fit is cut with dots.
- One dark panel with the rows Size (
986 KB,1.5 MB) and Built (2 Nov 2026). - For an installed app, one red Remove pill, 160 x 60 at y 412.
A row, a line or the words that are not known are left out, and what stays moves up. A file that is not installed has no Built row and no pill, and a version only when its data folder names one. BOT or a swipe down from anywhere on the card closes the details. TOP held does nothing there.
Remove
The Remove pill asks first, on a card of its own: Remove <name>?, the caption "It leaves the Dot." and, when a copy of the image stays on the SD card, "The SD card copy stays.", and one red Remove pill. BOT or a swipe down says no. The pill takes no answer for 400 ms (GUI_REMOVE_GUARD_MS), so the press that opened the question is not the answer.
The answer posts appmgr_erase_request() and shows a locked Removing card until AO_AppMgr has taken the app off the list, or 15 s (GUI_REMOVE_WAIT_MS) have passed. AO_AppMgr sends no answer to the erase, so AO_Gui asks the apps again every second and the listing ends the wait. A .bin of the same name on the SD card stays and shows again as a row that can be installed. HOME is the one input that leaves the Removing card early.
The return to Apps
A boot that comes back from an app lights Apps, not the face, in the app's folder with the app selected. Gui_returning holds the first frame for the installed list, for 800 ms at most. The return is a fresh unlock: the auto-lock count starts and no lock screen is shown. An app that belongs to no folder is focused at the root.
The empty launcher
When both lists are empty, the launcher has no rows. The screen shows one box instead: empty_box, in one of five looks.
| Look | The glass shows | What ends it |
|---|---|---|
GUI_EMPTY_LOOKING | "Looking for apps" alone | Both listings have answered |
GUI_EMPTY_KODY | "No apps yet", "Tap Kody to find apps", Kody looping at y 133 to 502 (the pack's empty role, 410 x 369) | TOP, released or held, or a tap on Kody |
GUI_EMPTY_LEAVING | The title and Kody sliding down, paused. 380 ms ease-in | The slide ends |
GUI_EMPTY_QR | The title, a QR code for https://apps.kode.diy, the address in text and "Copy apps to the microSD card" | 20 s without input (GUI_EMPTY_BACK_MS), or TOP again |
GUI_EMPTY_RETURNING | The title and Kody sliding up, paused. 380 ms ease-out | The slide ends |
- The box shows "Looking for apps" until both listings have answered, so the empty state never goes up while the card is still being read. It then settles on the Kody look once and does not blink on later rescans.
- The title stands at the same y in every look. The QR is 25 modules of 10 px in a 274 px square.
- A tap on Kody and TOP do nothing while "Looking for apps" shows or Kody is sliding.
- A pack with no
emptyanimation leaves the QR look alone. - The 20 s count uses the auto-lock's clock, so whatever holds the auto-lock holds it, and it runs even when the auto-lock is Never.
Kody plays here only while the box shows the Kody look, rows do not hide it, the launcher has landed and no card is over it. A curtain that is still moving over the launcher pauses him. The player takes a 605,160-byte buffer of PSRAM on its first play, and a board with apps never pays for it. While he loops, AO_Gui draws about 12.5 passes a second at 72 ms each; the QR look draws nothing.
The other inputs are the launcher's doors: BOT released, the pad's left and a swipe right go to the face, and the auto-lock locks the board. A drag down on an empty list pulls nothing, because the list has no rows: TOP held opens Settings there.
Specifications
The rail
The numbers of the rail, all named defines.
| Thing | Value |
|---|---|
| Row | 112 px high (KGUI_ROW_H), gap 0, so the pitch is 112 |
| Disc | 80 px (KGUI_RAIL_ICON) at x 30 |
| Name | At x 134 (KGUI_RAIL_NAME_X), 258 px wide |
| Focus line | y 251 (KGUI_RAIL_FOCUS_Y) |
| Edge | Rows with an icon above y 99 or below y 403 are pushed in. Whole 60 px (KGUI_RAIL_EDGE_FULL) from the edge, gone at 40 px past it |
| Block | 3 rows or fewer (KGUI_RAIL_BLOCK_MAX) stand centred and do not scroll |
| Ring | The kit's focus ring: Bold Yellow, 4 px wide, 4 px outside the disc |
| Icon | 64 x 64 RGB565A8 in a 12 KB slot, drawn at the disc's size on every row. Without one the row draws the app's colour with the first two letters of its name |
| Rows | GUI_ROWS_MAX 128 at most; folders KGUI_GROUP_MAX 10 at most |
How a row is painted
A row shows its name alone. The version and the size are the details'. Where each field comes from, strongest source first:
| Field | First source | Then | Default |
|---|---|---|---|
| Name | The app's data folder | The binary's manifest | Project name, or file name |
| Category | The data folder | The app_cat table, then the manifest | None; a loose row at the root |
| Version | The data folder | The image's app descriptor, installed apps only | Empty |
| Description | The data folder | Empty | |
| Icon | The data folder | The app's colour with the first two letters of its name | |
| Colour | The data folder | Medium Grey |
The code writes the weak sources as soon as it knows them and the data folder overwrites them last. The identity behind a row is always the project name or the file name, so two apps may share a display name. A colour that does not stand out from the black page is replaced by Medium Grey, and the glyph's ink is whichever of Soft Black and Light Grey reads better on the ground.
A file row appears when the file ends in .bin and is not a system image: kodeos.bin is About's OS update and c5_slave.bin is the radio firmware, on either medium. Until its manifest answers, a file counts as installed when its name without the extension matches an installed project name, ignoring case. After that the project name is compared. The fallback can show a row twice for a short time rather than miss one.
Gestures on the rail
The recognizer decides what every touch is, and AO_Gui moves the list itself (internals/gui/buttons-and-gestures).
| What | Value |
|---|---|
| A tap | A press that never goes farther than 16 px (KUI_GESTURE_TAP_PX) from where it landed. A press that travels is no tap and acts on nothing |
| A vertical drag | Claimed at 12 px (KUI_GESTURE_CLAIM_PX); the list follows the finger |
| A swipe right | Goes back when the finger went at least 40 px along it (KGUI_SIDE_STEP_PX) and the launcher still stands bare |
| A finger held on a row | 600 ms within 16 px, with no claim, opens the details of an app |
| The Settings pull | A drag down from the top 44 rows, or on a list at rest at its top; Settings follows the finger |
| Swipe left | At the root, goes to the face; in a folder, nothing |
Where the rows come from
Each listing is one event per entry followed by an end marker, in pages of 24. Every entry carries its place pos, and the marker says whether another page follows (more). AO_Gui asks the next page itself and paints nothing until the last.
- Position 0 starts a new list, a repeated entry changes nothing, and an end marker with no entries empties the list, which is how a region with every app erased shows empty.
- A request never empties a stored list, so a rescan is invisible until it has something to say: the list on the glass is always the last one that was true.
- Both owners answer in milliseconds or seconds, so the repaints wait until both have answered at least once and neither is in the middle of a round.
Rescans. Entering Apps from the face asks both lists again, because a .bin can be copied to the card while the list is off the glass. The entry skips the rescan while a round is still out. The way back from Settings does not rescan. After 10 s AO_Gui stops believing a round is out. The console's app_list does not reach AO_Gui, so an app erased from the console stays until the next rescan.
The metadata chains. Two chains ask for the look of a row, one question at a time:
- A manifest question to AO_Storage for each
.bin, because it allows one open file in the system. A refusal is no answer: AO_Gui asks again 500 ms later, up to 6 times. - One app-data question per row. An icon takes three of the four blocks of the jumbo event pool, so two exchanges at once would use the whole pool.
Gui_running handles the two listings, the manifest answer and the app-data answers, not Gui_browsing, because the exchanges outlive the screen: the face is on the glass while the first scans are in flight.
Gotchas
Example code
Install an app from the microSD card and watch the row appear:
- Copy a
.binbuilt for the Kode Dot to the microSD card and put the card in the slot. - On the face, tap anywhere. Apps slides in under "Looking for apps" while the card is read, then shows the file as a row. If the app names a category, it is inside a folder: press TOP to open it.
- Press the pad's down button until the row is in focus on the middle line. Hold TOP for a moment. The details open with the app's icon, name and version. Press BOT to close them.
- Press TOP. The write screen fills its edge round the glass, then the screen fades and the Kode Dot restarts into the app.
- Hold BOT and LEFT for about 1 s to reset the Kode Dot. kodeOS boots straight into Apps, in the app's folder with the app in focus. The app is now an installed row, listed first.
In the SDK
kgui_screen_launcher.c: the window and the empty box.kgui_list.candkgui_motion.c: the rail's objects and its model.kgui_group.c: the folders, their colours and glyphs.kgui_ao.c:Gui_browsing,repaint_list(),gui_select()and the two listing chains.