The normal way to get a programmable Android home screen is to keep whatever launcher came with the phone and bolt an automation app like Tasker onto the side of it. That combo is mature, it’s on the Play Store, it has a decade of community recipes behind it, and for most people it does the job without anybody having to touch a build system.
I went the other way and built Dikciz, a launcher that is the automation platform instead of something automation pokes at from outside. HTML widgets with a JavaScript bridge, sandboxed Lua scripts that react to phone events, a WebSocket and an MCP server on device loopback, and all of it configured through plain files in /sdcard/Dikciz/. Here’s how the two approaches actually stack up, one thing at a time.
What It’s Built On
A stock launcher is somebody’s closed product, and the automation app on top of it is somebody else’s. You live with both release schedules and neither one knows the other exists.
Dikciz is based on Fossify Launcher, GPLv3, because writing a launcher from zero means drowning in boring edge cases around app icons, drawers and Android’s home role. The whole Fossify source stays intact as the upstream base. All the Dikciz code lives in an isolated Gradle product flavour under app/src/dikciz/ (plus combined source sets like app/src/dikcizDebug/), so Fossify’s own sources, resources and translations stay replaceable. Pulling a newer Fossify release in goes through a checked route:
make dikciz-upstream-compare DIKCIZ_UPSTREAM_DIR=path/to/fossify-source
make dikciz-upstream-refresh-dry-run DIKCIZ_UPSTREAM_DIR=path/to/fossify-source
DIKCIZ_UPSTREAM_REFRESH=1 make dikciz-upstream-refresh \
DIKCIZ_UPSTREAM_DIR=path/to/fossify-sourceIt stages the source, verifies a backup, preserves Dikciz-owned paths and stops dead on a collision or a patch failure, and it refuses to replace anything unless the selected Fossify release marker is newer than the checked-in one. No “merge and hope” shit.
The build you actually run is dikcizDebug, package eu.psyb0t.dikciz.launcher.debug. That’s the one that keeps the control planes, the public config, the logs, the HTML bridge and the automation surface switched on.
Where the Config Lives
With a stock launcher, your home screen is a private database you never see. You get a settings screen, and the automation app keeps its own profiles in its own format. Diffing either one, backing it up as text or generating it from a script is somewhere between awkward and impossible.
In Dikciz every user-editable thing lives under /sdcard/Dikciz/, with config.json as the root. Pages, widgets, scripts, automation policies, themes, fonts, wallpaper, widget-library packages, auth config and logs each get their own path. Dikciz watches the tree and applies valid edits without an app restart. Push a file over ADB and the home changes. Keep it in git if you feel like it.
The bundled config carries hard limits so a fat-fingered edit can’t eat the phone: 24 pages max, 128 widgets per page, 8 HTML widgets per page, 1 MiB of config, 192 KiB per HTML widget document. If something does go to shit anyway, safe mode opens the bundled first page without overwriting your saved config, keeps diagnostics and logs reachable, and blocks home edits and device mutations until you sort it out.
Laying Out the Home Screen
Stock launchers give you a grid and a drag gesture. Fine for humans, useless for anything that wants to address a spot on the screen by name.
Dikciz pages sit on a two-axis grid and get addressed by coordinate: 1H1V is home, 2H1V is one to the right, 1H2V is one down. Every page is a native grid with a fixed viewport that never grows. Add a widget and it takes the first free cell, left to right and then down. Page full? The add gets refused. No overlap, no icons quietly shoved off screen.
What goes on a page:
- HTML widgets with local HTML, CSS, JavaScript and persistent state, resizable, with native long press actions and content-fitted height.
- Native app shortcuts and app groups. Drop one shortcut onto another and you get a group.
- Real third-party Android AppWidgets. They keep their own
AppWidgetHostViewand never get stuffed inside a WebView. - A native script dashboard listing your Lua scripts with status, last run, logs, and enable, disable and edit controls.
Themes, fonts (Inter, Space Grotesk, JetBrains Mono and Comic Neue are bundled, because of course), wallpaper, card styles and locks round it out. The Dikciz mark beside the page rail opens the app drawer, add to page, manage page and settings. The magnifier next to it does page search, listing every page as Name (address) like Notes (2H1V). The app drawer is native, not a WebView, and its actions cover add shortcut, app info, uninstall, and force stop, which only shows up when the device grants root.
Writing Your Own Widget
On the stock side, a custom widget means either writing a whole Android app with an AppWidgetProvider and RemoteViews, or installing yet another widget-maker app with its own editor. Both work. Both are a pain in the ass for a card that shows three numbers.
A Dikciz widget is a web page. Every HTML widget gets window.dikciz, window.selfWidget and a dikciz-ready event. From there it can:
- call any launcher command with
dikciz.command(type, fields), the same commands WebSocket and MCP clients use - run typed actions through
dikciz.dispatch({ actions }) - read the page map with
dikciz.pages()anddikciz.home() - patch another widget’s state with
dikciz.patchWidgetState(address, values) - change rendered DOM without saving it, via
dikciz.patchDom(address, selector, values) - publish and subscribe to events with
dikciz.emitEventanddikciz.onEvent
const result = await dikciz.patchDom(
"1H1V-device-card",
"#temperature",
{
text: "18 C",
className: "fresh",
attributes: { "data-updated": "now" }
},
);State patches update the existing document. They don’t reload the WebView, so the widget doesn’t flash like a cheap motel sign every time a value changes.
Reacting to Phone Events
This is where the bolt-on automation app earns its reputation. Profiles, contexts, tasks, a huge library of triggers. Its weak spot is that the launcher has no idea any of it exists, so the automation pokes the home screen from the outside and hopes the result looks right.
In Dikciz the event handling is part of the launcher. Lua isn’t a widget type, it runs as scripts from the public scripts tree on LuaJ. A script receives an event and returns a status plus a list of typed actions:
function on_event(event)
if event.type ~= "example.pipeline.request" then
return { status = "Waiting for a pipeline request" }
end
local message = event.payload.message or "Pipeline request received"
return {
status = "Updated the widget",
actions = {
{
type = "patchWidget",
widgetAddress = "1H1V-example-widget",
values = { state = { message = message } }
},
{
type = "emitEvent",
event = "example.pipeline.done",
payload = { message = "Lua finished: " .. message }
}
}
}
endThe event list is long as hell: time, alarms, battery, charging, thermal status, screen on and off, connectivity, Bluetooth, sensors, location, Health Connect steps, calendar, contacts, phone state, incoming SMS, clipboard, notifications posted and removed, media sessions, accessibility windows, device admin state, widget changes, manual triggers and any custom event you invent.
The actions are typed too: selectPage, patchWidget, patchState, patchDom, launchApp, appAction, postNotification, sendSms, explicitIntent, emitEvent, mediaControl, mediaVolume, notificationControl, lockDevice and more. Because the script is talking to the launcher directly, patchWidget lands on a named widget at a named address instead of some approximation of “the thing in the top left”.
How Much a Script Can Break
Automation apps tend to hand a task a big pile of power: run shell, write files, fire arbitrary intents. Handy, and also exactly how a copied recipe from some forum ends up doing something you never read.
Dikciz is stingier on purpose. Every action and every event capability a script uses has to be listed in that script’s public automation policy, and the Lua runtime is boxed. require, package, load, loadfile, dofile, debug and print are gone. No shell, no filesystem, no raw Android objects. Typed actions or fuck off.
Logs land as bounded JSON Lines under /sdcard/Dikciz/logs/, readable from the dashboard, the Settings viewer, WebSocket scriptLogs and MCP dikciz_script_logs. Credential-shaped values get redacted before they hit the file. A failed Lua run can post an Android notification, with a per-script quiet period so it doesn’t spam you to death.
Letting an AI Agent Drive
This is the dimension I actually built the thing for. Hand a stock launcher to an LLM agent and the usual answer is “take a screenshot, guess coordinates, tap, pray”. That’s not automation, that’s a drunk guy with a stylus.
Dikciz runs two versioned listeners on device loopback:
- WebSocket at
ws://127.0.0.1:19001/v1/automation - Streamable HTTP MCP at
http://127.0.0.1:19002/mcp
Both hit the same command target: config, semantic UI snapshots and interaction, pages, widgets, screenshots, UI dumps, diagnostics, script logs, the app catalogue, app actions, Android intents and shell commands. The MCP side exposes tools like dikciz_snapshot, dikciz_tap, dikciz_find, dikciz_wait_for, dikciz_screenshot, dikciz_app_catalogue and dikciz_shell. Point an agent at it and it inspects the launcher semantically instead of guessing pixels.
Shell commands ask for su by default. Pass root: false and they run as Dikciz’s own Android app user. Whether root actually exists and gets approved is the phone’s call, not mine.
Turn on Dikciz’s Accessibility service and it reaches past its own home screen too: local clients get a bounded tree of the active window from any app, plus accessibilityAction, accessibilityGesture and accessibilityGlobalAction. Lua gets the same window as context.accessibility.
Since “anything on loopback can drive my phone” is a stupid default for a device you carry around, you can require a bearer password for both control planes from Dikciz Settings. The public config stores a bcrypt verifier, never the plaintext.
The repo also ships a dikciz-launcher agent skill that teaches an agent to install the APK over ADB and drive the launcher, with setup and capability references, and a separate dikciz-android-lab skill that routes source builds and emulator work to android-lab. Run your agent in something like aicodebox and that’s the whole loop: agent reads the skill, connects to the MCP endpoint, runs your phone.
Getting It Onto a Phone
Here the stock combo wins outright: tap install in the Play Store and you’re done. Dikciz doesn’t go through Google Play and it never will. You build it.
I don’t install Android Studio. Ever. The build and emulator side runs on android-lab, my Docker rig for the Android SDK and emulator. The Makefile wants locally built android-lab:0.13.0 and android-lab:0.13.0-emulator-api36 images, and if either is missing it stops and points you at android-lab’s build instructions instead of pulling crap on your behalf.
git clone https://github.com/psyb0t/dikciz-launcher.git
cd dikciz-launcher
make build
make dikciz-runmake build assembles dikcizDebug. Tool containers take the Docker host’s CPU count capped at 10, with matching Gradle workers. On a weaker box:
make build ANDROID_LAB_TOOL_CPU_LIMIT=2 ANDROID_LAB_GRADLE_MAX_WORKERS=2make dikciz-run installs it on the shared emulator, grants file access, sets it as Android Home and forwards the control planes to the host on 19101 (WebSocket) and 19102 (MCP). The emulator screen is in your browser at:
http://127.0.0.1:61326/vnc.html?autoconnect=true&resize=scaleThen talk to it with the bundled client:
make control CONTROL_ARGS='websocket controlStatus --port 19101 --pretty'
make control CONTROL_ARGS='mcp-tools --port 19102 --pretty'
make control CONTROL_ARGS='websocket scriptLogs --port 19101 --pretty'If you turned auth on, export DIKCIZ_REMOTE_BEARER_TOKEN from an ignored env file. Don’t put it in a command argument like an animal. make dikciz-reset nukes only Dikciz and /sdcard/Dikciz and boots the starter home. make stop stops the stack without deleting the saved phone. The state (Gradle cache, signing identity, emulator disk, artifacts) lives in .android-lab/.
Tests are UI tests against that same emulator, with narrow targets like make dikciz-test-accessibility or make dikciz-test-app-drawer-entry, and make dikciz-test for the broad suite, which dumps screenshots and UI dumps into .android-lab/shared/artifacts/ and resets the phone to the starter home after, pass or fail.
When to Keep the Stock Launcher and Tasker
If you want a phone that works the day you buy it, installs from the Play Store and gets automation through a GUI with years of shared recipes behind it, keep the stock launcher and the bolt-on app. Same if you never plan to edit a config file, write a line of Lua or point an agent at your phone. Dikciz asks you to build it yourself in Docker, and it pays that back only if you want the home screen to be files, code, a socket and an MCP server.
If that last part sounds like what you’ve been missing, it’s at github.com/psyb0t/dikciz-launcher.