- QML 54.2%
- TypeScript 38.6%
- JavaScript 7%
- Dockerfile 0.2%
Replace hardcoded mariadb://todosync:todosync@... URLs with placeholders and --env-file .env in Docker commands. Add env.example as a template for local configuration. |
||
|---|---|---|
| src | ||
| tests | ||
| todo | ||
| todosync-app | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| Dockerfile | ||
| env.example | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| registry.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
todosync
Multi-device todo list with real-time sync. A REST API server + Noctalia Shell plugin that keeps todos synchronised across machines via a central MariaDB database.
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Desktop │ │ REST API │ │ Laptop │
│ (Noctalia) │◄───►│ + MariaDB │◄───►│ (Noctalia) │
│ Plugin │ │ (hetzner1) │ │ Plugin │
└─────────────┘ └──────────────┘ └─────────────┘
Quick Start
1. Run the server
# Build & run with Podman
npm run build
podman build -t todosync .
podman run -d \
--name todosync \
-p 3456:3456 \
-e DATABASE_URL="mariadb://user:pass@host:3306/todosync" \
todosync
# Or directly (dev)
npm run dev
2. Install the plugin
# Symlink the plugin into Noctalia
ln -sfn ~/code/todosync/todo ~/.config/noctalia/plugins/todo
Register in ~/.config/noctalia/plugins.json:
{
"todo": {
"enabled": true,
"sourceUrl": "https://github.com/maikelthedev/todosync"
}
}
Restart Noctalia (from fish shell):
kill (pgrep -f quickshell | head -1)
sleep 3
WAYLAND_DISPLAY=wayland-1 nohup noctalia-shell &>/dev/null &
Open Todo Settings → set http://your-server:3456 → Save. The plugin connects immediately — no second restart needed.
3. Done
Todos sync automatically across all machines connected to the same server.
Project Structure
todosync/
├── src/ TypeScript REST API server
│ ├── app.ts Express app factory
│ ├── db.ts Schema & database helpers
│ ├── server.ts Entry point
│ └── types.ts Shared types
├── todo/ Noctalia Shell plugin
│ ├── manifest.json Plugin definition
│ ├── Main.qml Core logic, sync engine, IPC handlers
│ ├── Panel.qml Overlay panel with status indicator
│ ├── BarWidget.qml Compact bar widget
│ ├── DesktopWidget.qml Full desktop widget
│ ├── Settings.qml Settings UI (server URL, priorities, pages)
│ ├── logic.mjs Pure JS business logic (also tested by Vitest)
│ └── i18n/ Translations
├── tests/ Test suite (Vitest + supertest)
│ ├── todos.test.ts REST API: todos
│ ├── pages.test.ts REST API: pages
│ ├── export.test.ts REST API: export & health
│ ├── logic.test.ts Pure business logic (34 tests)
│ └── setup.ts Shared MariaDB pool for tests
├── registry.json Plugin registry for Noctalia auto-discovery
├── Dockerfile Container build
└── package.json
How Sync Works
Architecture
The sync uses a central server pattern. All plugin instances connect to the same REST API, which stores data in MariaDB.
Plugin → Server (Push)
- User adds, edits, or deletes a todo
- Main.qml calls
saveTodos()orsavePages() - A 500ms debounce timer batches rapid changes
- After 500ms of inactivity,
POST /api/todos/batchandPOST /api/pages/batchare called - The batch endpoints replace ALL data atomically inside a transaction
Server → Plugin (Pull)
- A 5-second poll timer (
GET /api/export) checks for remote changes - If the server state differs from local state, local state is replaced
- State comparison uses
JSON.stringifyon both sides — quick and avoids flicker
Offline Handling
- When the server is unreachable, the plugin uses its last cached
settings.jsonon disk - A 30-second retry timer keeps trying to reach the server
- A status bar at the bottom of the panel shows connection state:
- Hidden — connected, healthy
- Blue "Syncing…" — push or pull in progress
- Red "Server offline — retrying…" — server unreachable
On Startup
Component.onCompletedreadsserverUrlfrom saved settings- If set, calls
GET /api/exportto load full state from server - On success: display server data, start polling
- On failure: fall back to local cache, start retry timer
Settings Save = Instant Connect
When the user sets a server URL in settings and saves, Settings.qml calls mainInstance.connectToServer() directly — no shell restart needed.
REST API
Todos
| Method | Path | Description |
|---|---|---|
GET |
/api/todos |
List all todos |
POST |
/api/todos |
Create a todo |
PUT |
/api/todos/:id |
Update a todo (text, completed, priority, details, pageId) |
DELETE |
/api/todos/:id |
Delete a todo |
POST |
/api/todos/batch |
Replace all todos atomically |
POST |
/api/todos/clear-completed |
Remove completed todos |
DELETE |
/api/todos |
Remove all todos |
Pages
| Method | Path | Description |
|---|---|---|
GET |
/api/pages |
List all pages |
POST |
/api/pages |
Create a page |
PUT |
/api/pages/:id |
Rename a page |
DELETE |
/api/pages/:id |
Delete a page (moves todos to default) |
POST |
/api/pages/batch |
Replace all pages atomically |
Other
| Method | Path | Description |
|---|---|---|
GET |
/api/export |
Full export (todos + pages) |
GET |
/api/health |
Database health check |
Todo fields
{
"id": 1783137530132121,
"text": "buy milk",
"completed": false,
"createdAt": "2026-07-04T03:58:50.132Z",
"lastModified": "2026-07-04T03:58:50.132Z",
"pageId": 0,
"priority": "medium",
"details": ""
}
Page fields
{
"id": 0,
"name": "General"
}
Page ID 0 is the default "General" page — it always exists and cannot be deleted.
Plugin Features
- Bar widget — shows active todo count in the system bar; click to open panel
- Panel — full todo management with pages, priorities, drag-to-reorder, search
- Desktop widget — floating desktop todo list with expand/collapse and per-page views
- Control center widget — quick-access button
- Pages — group todos into custom pages (Work, Personal, Shopping, …)
- Priority system — high / medium / low, each with configurable colours
- Export — markdown or JSON to file
- Sync status bar — shows connection state at the bottom of the panel
- Auto-reconnection — retries every 30s if server goes offline
- Focus overlay — top-right HUD showing your currently active tasks with gentle pulse animation. Designed for people with ADHD who juggle multiple things at once — mark any task as "in focus" and it stays visible over everything, even fullscreen apps. Customise or toggle via IPC.
IPC Commands
Control todos programmatically from scripts or the command line via Quickshell IPC:
# Read operations
noctalia-shell ipc call plugin:todo getTodos # all todos as JSON
noctalia-shell ipc call plugin:todo getTodo <id> # single todo as JSON
noctalia-shell ipc call plugin:todo getPages # all pages as JSON
noctalia-shell ipc call plugin:todo getCount # {total, active, completed}
# Create
noctalia-shell ipc call plugin:todo addTodoDefault "Buy groceries"
noctalia-shell ipc call plugin:todo addTodo "Write report" "high" <pageId>
noctalia-shell ipc call plugin:todo addPage "Work"
# Update
noctalia-shell ipc call plugin:todo setTodoText <id> "New text"
noctalia-shell ipc call plugin:todo setTodoPriority <id> "high"
noctalia-shell ipc call plugin:todo setTodoCompleted <id> true
noctalia-shell ipc call plugin:todo toggleTodo <id>
noctalia-shell ipc call plugin:todo renamePage <pageId> "New Name"
# Delete
noctalia-shell ipc call plugin:todo removeTodo <id>
noctalia-shell ipc call plugin:todo removePage <pageId>
noctalia-shell ipc call plugin:todo clearCompleted
noctalia-shell ipc call plugin:todo clearAll
# Panel
noctalia-shell ipc call plugin:todo togglePanel
Note: Todo and page IDs are 13+ digit timestamp numbers. They fit in QML
var/doublebut not in QMLint(32-bit). Seetodo/logic.mjsfor full ID constraint documentation.
Configuration
Accessible via Settings → Todo:
- Show Completed — toggle visibility of completed items
- Show Background — desktop widget background opacity toggle
- Custom Priority Colors — enable/disable custom colours per priority level
- Priority Colors — individual colours for high, medium, and low
- Export Format — markdown or JSON
- Export Path — directory for exported files
Testing
70 tests total — all with real database access:
npm test # run all tests (Vitest)
npm run test:watch # watch mode
npx vitest run tests/logic # logic tests only (34 tests, ~400ms)
Test structure
| File | Tests | What it covers |
|---|---|---|
tests/logic.test.ts |
34 | Pure business logic (no DB, no API) |
tests/todos.test.ts |
20 | REST API for todos |
tests/pages.test.ts |
13 | REST API for pages |
tests/export.test.ts |
3 | Export & health endpoints |
The business logic tests run in ~400ms and don't need a database — they test the pure JavaScript functions in todo/logic.mjs.
Deployment
Option 1: Podman (recommended)
npm run build
podman build -t todosync .
podman run -d \
--name todosync \
--restart unless-stopped \
-p 3456:3456 \
-e DATABASE_URL="mariadb://user:pass@host:3306/todosync" \
todosync
Option 2: PM2
npm run build
pm2 start dist/server.js --name todosync
pm2 save
pm2 startup
Option 3: NixOS systemd service
Add a systemd.services.todosync entry to your NixOS config with EnvironmentFile pointing to a .env with DATABASE_URL.
Environment Variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
✅ | Production database connection URL (set in .env) |
TODO_SYNC_TEST_DATABASE_URL |
✅ (for tests) | Test database connection URL (set in .env) |
TODO_SYNC_PORT |
❌ (default: 3456) |
Server port |
TODO_SYNC_HOST |
❌ (default: 0.0.0.0) |
Server bind address |
Security: Copy
env.exampleto.envand fill in your real credentials. Never commit.envto version control.
Development
# Install dependencies
npm install
# Run server with hot reload
npm run dev
# Run tests
npm test
# Build TypeScript
npm run build
The plugin can be developed with hot reload — enable debug mode by clicking the Noctalia logo 8 times in Settings > About, then enable dev mode for the plugin. QML changes apply on save without restarting the shell.
Repositories
- GitHub: github.com/maikelthedev/todosync
- Forgejo: git.maikel.dev/maikelthedev/todosync