Status: scoping draft. v2.0 ships first-party widgets only — same code path as HomeSpike itself, runs in lomiri's process, no Click integration yet. v2.1 adds the out-of-process Click-widget API (Mir-surface compositing).
Scope: rendering, layout, lifecycle, refresh, persistence, and the sample widget set HomeSpike ships in v2.0.
Let HomeSpike host widgets — small QML surfaces (clock, battery, recent mail, calendar agenda, etc.) — that live alongside icon tiles on the home grid. v2.0 is TeamIDE-first-party only: HomeSpike ships the widgets in its own payload, users can't write their own yet. The contract we build here is what v2.1's third-party tier will plug into.
Widgets are QML files loaded into HomeSpike's process via Loader.
Same security posture as HomeSpike itself: full power, full trust,
runs as phablet. We accept this risk for first-party widgets;
sandboxing comes in v2.1 with the Mir-surface tier.
/opt/home-spike/widgets/
├── <widget-id>/ (one directory per widget)
│ ├── widget.json (manifest)
│ ├── Widget.qml (the widget's UI)
│ ├── icon.svg (shown in the "add widget" picker)
│ └── (any other resources the widget needs)
└── ...
Naming convention: <widget-id> = reverse-domain (e.g. dev.teamide.clock).
Minimal v2.0 schema:
{
"id": "dev.teamide.clock",
"name": "Clock",
"description": "A clock face — analog or digital.",
"version": "1.0",
"author": "TeamIDE",
"icon": "icon.svg",
"entry": "Widget.qml",
"sizes": ["2x2", "4x2"],
"default_size": "2x2",
"refresh_seconds": 60,
"tap_target": "application:///dev.teamide.clock.desktop"
}Field meanings:
id— globally unique, reverse-domain.sizes— array of WxH strings the widget can render at. Coordinate system = HomeSpike's existing grid cells (1 cell ≈ icon footprint).default_size— whatAdd widgetuses on first placement.refresh_seconds— HomeSpike callswidget.refresh()at most this often when the widget is visible (capped to 10s minimum to protect battery).0means push-only.tap_target— URL fired when the user taps the widget (typically the parent app's.desktop). Optional — omit if the widget itself handles taps internally.
Inside Widget.qml, HomeSpike exposes these to the root Item:
Item {
id: widget
anchors.fill: parent // HomeSpike sets parent geometry for you
// ---- Injected by HomeSpike ----
// Geometry the widget should render to. Bound to the cell size the
// user picked (e.g. 2x2 cells of HomeSpike's grid).
// Read-only: widget.width, widget.height come from HomeSpike.
// Current size string (matches one of the manifest's "sizes" entries).
property string size: "2x2"
// True while the widget's containing page is the current page in
// HomeSpike's pageview. Use this to suspend timers / animations
// when off-screen.
property bool active: true
// Edit-mode flag — true when the user is rearranging things.
// Widgets can use this to grey themselves out, show a remove
// badge, etc.
property bool editMode: false
// ---- Functions HomeSpike calls ----
// Pull-refresh trigger (scheduled by HomeSpike's per-widget timer,
// OR fired explicitly by the widget via pingRefresh()). Widget
// implements this to re-read its data source.
function refresh() { /* override */ }
// ---- Signals the widget can emit ----
// Bubble a "data changed, please redraw me" event out to HomeSpike
// when push-style refresh applies (e.g. file watcher fired).
signal pingRefresh()
// Override the manifest's tap_target for a specific tap region.
// Optional — leave unbound to fall through to manifest default.
signal tapHandled(string url)
}All standard QML/Qt APIs are available (file IO via XMLHttpRequest,
GSettings, Timer, Qt.labs.settings, etc). Widget rendering is just QML
— no special component palette to learn.
- Discovery — at startup, HomeSpike scans
/opt/home-spike/widgets/*/widget.jsonand builds an in-memory registry. Re-scan on user-triggered "Refresh widget list" (manual, in Settings → HomeSpike). - Placement — user enters edit mode, taps + Add widget, picks
from the registry, taps a free spot on the page. HomeSpike adds an
entry to that page's persist with
{type: "widget", id, size, col, row}. - Instantiation — when a page renders, each widget entry instantiates
its
Widget.qmlviaLoader. - Visible vs off-page — HomeSpike sets
widget.active = trueonly for the current page's widgets. Off-page widgets getactive = falseand their refresh timer pauses. - Refresh schedule — HomeSpike maintains one
Timerper visible widget instance, interval =manifest.refresh_seconds(min 10s). On tick, callswidget.refresh(). Widget can alsowidget.pingRefresh()to demand an out-of-cycle refresh (e.g. after a file-watcher event). - Removal — user taps the × badge in edit mode → entry removed
from persist →
Loaderdestroys the widget instance.
Widgets coexist with icon tiles in the same per-page layout:
- Auto-fill mode (default): widgets are skipped from auto-flow.
Pages with widgets behave like
snapmode — explicit position required. (Open: should we just disallow widgets in auto-fill mode and force the user to switch? Decision pending; safest = allow but treat widget-occupied cells as "do not flow into".) - Snap-to-grid mode: widget occupies its declared
WxHblock of cells starting at its(col, row). Other tiles flow around occupied cells. - Place-anywhere mode: widget gets
xFrac/yFracpluswFrac/hFrac(we extend the per-tile schema). Drag corners to resize.
Persistence extension (per-page bag, snap mode):
{
"snap": [
{"appId": "phone", "col": 0, "row": 0},
{"type": "widget", "id": "dev.teamide.clock", "size": "2x2", "col": 1, "row": 0},
...
]
}type: "widget" is the new discriminator. Old entries without type
are still treated as tile (appId-driven).
- + Add widget button at the top of the edit-mode chrome (next to the existing Done pill).
- Tapping it opens a widget picker modal (scrollable grid of registered widgets with icon + name).
- Drop on a free spot on the current page → placement uses the widget's
default_size. - × badge on widget in edit mode (same as tiles).
- Resize handles on widget corners (only when the manifest declares
multiple sizes). Snap to the nearest valid
WxHfrom the manifest.
Combination of push + pull, gated on visibility, capped on rate:
| Scenario | What happens |
|---|---|
| Widget is on the current page | Timer ticks; refresh() called on schedule |
| Widget is on a non-current page | Timer paused; active = false |
| User swipes to widget's page | active → true; one immediate refresh() then resume schedule |
Widget fires pingRefresh() (push) |
Out-of-cycle refresh() if active; queued for next visibility if not |
| Multiple visible widgets refresh same time | Allowed; QML handles concurrency naturally |
| Refresh interval requested < 10s | Clamped to 10s (battery floor) |
| HomeSpike disabled via Settings toggle | All widget timers stopped; widgets hidden |
To validate the API + give users something out-of-box. Three widgets:
dev.teamide.clock— analog or digital clock. Sizes:2x2,4x2. Refresh: 60s (the minute hand). Reads system time, no IPC.dev.teamide.battery— battery level bar + percentage. Sizes:2x1. Refresh: 30s. Reads/sys/class/power_supply/battery/capacity.dev.teamide.calendar-agenda— next 3 calendar events. Sizes:2x2,4x2. Refresh: 300s. Reads from EDS (Evolution Data Server, the UT calendar backend) via D-Bus.
All three exercise different data sources (none, file, D-Bus) so the API surface gets stretched.
Today's per-page bag has autoFill[], snap[], free[]. Widgets
extend snap[] and free[] entries with a type field:
{
"snap": [
{"appId": "phone", "col": 0, "row": 0},
{"type": "widget", "widgetId": "dev.teamide.clock", "size": "2x2", "col": 1, "row": 0}
],
"free": [
{"appId": "phone", "xFrac": 0.1, "yFrac": 0.1},
{"type": "widget", "widgetId": "dev.teamide.clock", "wFrac": 0.5, "hFrac": 0.3, "xFrac": 0.5, "yFrac": 0.1}
]
}autoFill doesn't grow widget support in v2.0 (we just skip widgets
when populating auto-fill pages — open question on this).
Migration: existing entries with no type field continue to render
as tiles. Forward-compatible read; legacy HomeSpike installs reading
new pages just ignore widget entries silently (no rendering, no crash).
New section in HomeSpike's settings overlay:
- Widgets — toggle each installed widget on/off globally (kill all instances of a widget that's misbehaving).
- Widget refresh policy — global cap "max refresh frequency" (default 30s; user can raise to 60s, 5min, 15min for battery).
- Click-app widget tier — the out-of-process Mir-surface compositing story for third-party widgets. v2.0 is pure first-party.
- Widget marketplace UI — discovery / install / update of widgets beyond what HomeSpike ships. v2.0 widgets ship in HomeSpike's payload.
- Widget configuration — per-instance settings (e.g. "show seconds on the clock", "show next 5 events not 3"). v2.0 widgets are config-less; v2.1 adds a per-widget settings sheet.
- Inter-widget communication — widgets don't talk to each other. Each widget owns its own data.
- Background data fetch — when no widget instance is visible anywhere, no fetch happens. v2.x might add background daemons later.
- Widget sizes — fixed list or freeform? I propose fixed (
1x1,2x1,2x2,4x2,4x4) for layout sanity. Open to a freeWxHif the design needs it. - Auto-fill behavior — disallow widgets in auto-fill mode, or keep but skip them from flow? Skip is friendlier but the math gets weird when you have a 4x2 widget on a flow-layout page.
- Resize gesture — drag a corner (mobile-friendly?) vs long-press then choose size from a menu (matches iOS WidgetKit's edit flow)?
- Page-dot indicator behavior — should pages with widgets get a different marker so the user knows which pages have heavier content?
- Per-widget enable toggle in HomeSpike Settings — yes, useful kill-switch. Or skip and rely on user removing the widget from the home grid?
- Crash isolation — first-party in-process means a buggy widget can hang HomeSpike. Acceptable for v2.0 since we ship the widgets; v2.1's out-of-process tier solves this for third-party.
- Versioning — what happens when a widget's
widget.jsonschema evolves? Maybe a top-levelschema_versionfield on the manifest.
- Three TeamIDE widgets shipping in HomeSpike's payload.
- User can add/remove/resize them in edit mode.
- They auto-refresh on schedule, pause off-page.
- HomeSpike Settings has a per-widget kill-switch.
- All three widget types (no-data, file-data, D-Bus-data) work cleanly.
WidgetAPI.mdis the contract that v2.1's Click-app tier extends — sameWidget.qmlAPI, just hosted in a different process.