Skip to main content

Config-driven, zero-dependency web GUI framework with dockable panes

Project description

mkui

PyPI Python License

A config-driven, dependency-free web GUI framework with a floating-frame workspace and dockable panes inside each frame. Designed to pair with mkio — the same project's TOML-driven microservice backend — but works against any backend (or none at all).

Model

mkui-app
├── mkui-menubar
├── mkui-workspace
│   ├── mkui-frame  ← floating, movable, resizable. Clamped to workspace.
│   │   └── layout tree
│   │       └── TabGroup
│   │           ├── tab bar
│   │           └── mkui-pane  ← leaf content host
│   ├── mkui-frame
│   │   └── split (h | v)
│   │       ├── TabGroup → panes
│   │       └── TabGroup → panes
│   └── ...
└── mkui-statusbar

Top-level windows are frames — floating chrome with 8-way resize. There is no dedicated titlebar: every tab bar at the top edge doubles as a drag region, and the right-most one also carries the window controls. Frames don't dock into each other. Inside each frame lives an independent, normalized layout tree of splits, tab groups, and panes. Docking — splitting, tabbing, tearing out — happens entirely inside frames.

Design commitments (things meant to hold up indefinitely):

  • Structural invariant: every pane leaf sits inside a tab group. A single-pane frame is a single-tab group. This removes a whole class of special cases from the renderer and the drop logic.
  • Proportional resize by construction: frame positions are fractions of the workspace rect, and split ratios sum to 1. Resizing the browser rescales every frame, every split, and every pane with no extra code.
  • Viewport clamping invariant: every frame move/resize passes through a single clampToDock helper. Frames cannot escape the workspace — shrinking the viewport drags stragglers back in.
  • Stable pane identity: pane elements live in a workspace-owned pool and are re-parented via appendChild when re-docked. Content state, subscriptions, scroll position, and DOM focus all survive.
  • Zero runtime dependencies. Web Components, so it drops into React / Vue / Svelte / vanilla identically.

Interactions

  • Top tab row → drag the whitespace next to the tabs to move the frame (clamped). Double-click the same region to toggle maximize. Frame edges/corners → 8-way resize (clamped, min 160×80).
  • Dragging a tiled or maximized frame restores it to its pre-tile size under the cursor on first motion. Resize handles or explicit maximize-toggle also clear the restore state.
  • Frame close button → closes the frame; panes inside are parked in the pool (state preserved) and can be brought back by code.
  • Tab click → switch active pane in that tab group.
  • Tab drag within the bar → reorders the tab in its group. If the cursor leaves the tab bar by more than a few pixels, the pane is torn out into a new frame at the cursor.
  • Alt+Shift+Left / Alt+Shift+Right → move the active tab left or right within its group (acts on the top-most frame).
  • Dragging a torn-out (or any single-pane) frame over another frame shows drop zones: edges split, center adds as a tab. Release to dock.
  • Splitter drag → resize the ratio between two children of a split.
  • Any mousedown inside a frame raises it to the top of the z-order. The top frame gets an accent border; within it, the last-clicked tab group's active-tab underline stays at full accent while others dim — that's the bar keyboard hotkeys act on.

Configs

mkui's runtime input is JSON. When the backend is mkio, add config_dir to your server.toml and point your HTML at /config/client.json — mkio reads client.toml from that directory and serves it as JSON. The browser never needs a TOML parser. For other backends, author or generate client.json directly.

Minimal config:

{
  "app":     { "title": "Trading desk", "theme": "dark" },
  "menubar": [{ "label": "File", "items": [{ "label": "Quit", "action": "app.quit" }] }],
  "statusbar": { "left": [{ "type": "text", "bind": "status.message" }] },

  "panes": {
    "orders":    { "title": "Orders", "type": "mkio-table", "service": "all_orders", "protocol": "query" },
    "chart":     { "title": "Chart",  "widgets": [{ "type": "text", "text": "Chart goes here" }] },
    "inspector": { "title": "Inspector", "widgets": [{ "type": "text", "text": "Properties" }] }
  },

  "frames": [
    {
      "id": "main",
      "x": 0.05, "y": 0.05, "w": 0.65, "h": 0.9,
      "layout": { "type": "tabs", "active": 0, "children": ["orders", "chart"] }
    },
    {
      "id": "aux",
      "x": 0.72, "y": 0.05, "w": 0.23, "h": 0.9,
      "layout": { "type": "tabs", "children": ["inspector"] }
    }
  ],

  "mkio": { "url": "ws://localhost:8080/ws" }
}

Frame positions (x, y, w, h) are fractions of the workspace rect.

Statusbar

statusbar is a top-level object with left and right widget arrays, plus an optional bindStyle map that binds CSS properties to state paths:

"statusbar": {
  "left":  [{ "type": "text", "bind": "status.message" }],
  "right": [{ "type": "text", "text": "v0.1" }],
  "bindStyle": { "background": "status.background", "color": "status.color" }
}

Each bindStyle entry subscribes to the given state path. When the value changes, the CSS property is set as an inline style on <mkui-statusbar>. Setting the state value to null (or empty string "") removes the inline override, reverting to the stylesheet default. Empty string is treated as null to support TOML configs, which have no null literal.

mkio connection state

When config.mkio.url is set, <mkui-app> automatically connects to the mkio server and verifies its identity. Connection is two-phase:

  1. Connect — WebSocket opens, mkio.connected becomes true, and the connected state map is applied immediately.
  2. Verify — an async _mkio request confirms the server is a genuine mkio server and optionally checks its name, version, and protocol against expectations declared in config.mkio.expect. On success mkio.verified becomes true; on failure the incompatible state map is applied. Verification re-runs on every reconnect.
"mkio": {
  "url": "ws://localhost:8080/ws",
  "expect": {
    "name": "order-book",
    "version": "1.0",
    "protocol": "1.0",
    "mkio": "0.1"
  },
  "connected":    { "status.message": "Connected", "status.background": null },
  "incompatible": { "status.message": "Wrong server", "status.background": "#cc0000" },
  "disconnected": { "status.message": "Disconnected", "status.background": "#858585" }
}

The expect keys are all optional. name is checked by exact match; version, protocol, and mkio use semver-compatible matching (delegated to the server's _mkio service). When expect is absent, the _mkio query still runs to confirm it is an mkio server and to populate mkio.server.* state paths (name, version, protocol, mkio).

The _mkio request has a configurable timeout (config.mkio.timeout, default 5000 ms) — non-mkio servers that don't respond are detected as incompatible.

State maps default to { "status.message": "Connected" }, { "status.message": "Disconnected" }, and { "status.message": "Incompatible server" }. Combine with statusbar.bindStyle to change the statusbar appearance on disconnect or server mismatch.

Menubar

menubar is a top-level array. Each element is a dropdown menu with a label and an items array:

"menubar": [
  {
    "label": "File",
    "items": [
      { "label": "New Frame", "action": "demo.newFrame" },
      { "sep": true },
      { "label": "Open Recent", "items": [
          { "label": "foo.txt", "action": "demo.open", "args": "foo.txt" }
        ]
      },
      { "label": "Quit", "action": "app.quit" }
    ]
  },
  {
    "label": "Window",
    "items": [
      { "label": "Cascade", "action": "window.cascade" },
      { "label": "Tile", "items": [
          { "label": "Horizontal", "action": "window.tileH" },
          { "label": "Vertical",   "action": "window.tileV" },
          { "label": "Grid",       "action": "window.grid" }
        ]
      }
    ]
  }
]

Item keys:

Key Type Description
label string Display text
action string Action name fired on click (leaf items only)
args any Optional argument passed to the action handler
items array Child items — makes this a submenu (opens on hover)
sep boolean true renders a horizontal separator line

Any item with an items array is a submenu; submenus nest arbitrarily. Leaf items (no items) fire action on click via app.fireAction().

Built-in actions: app.quit, pane.show (takes a pane ID as args — switches to that pane's tab and raises its frame, or opens a new frame if the pane is parked/closed), window.tileH, window.tileV, window.grid, window.cascade. Register custom actions with app.registerAction(name, fn).

A typical Window menu lists each pane for quick access:

{ "label": "Window", "items": [
  { "label": "Explorer",  "action": "pane.show", "args": "explorer" },
  { "label": "Console",   "action": "pane.show", "args": "console" },
  { "sep": true },
  { "label": "Cascade",   "action": "window.cascade" }
]}

Themes

dark and light are built-in. To ship additional themes, list them under app.themes — each entry is a flat object of CSS custom property overrides, applied as inline styles on <mkui-app> so every descendant inherits them:

{
  "app": {
    "theme": "solarized",
    "themes": {
      "solarized": {
        "--mkui-bg":        "#002b36",
        "--mkui-bg-alt":    "#073642",
        "--mkui-bg-hover":  "#0a4350",
        "--mkui-fg":        "#93a1a1",
        "--mkui-fg-mute":   "#586e75",
        "--mkui-border":    "#0a4350",
        "--mkui-accent":    "#268bd2",
        "--mkui-accent-fg": "#fdf6e3",
        "--mkui-focus":     "#eee8d5"
      }
    }
  }
}

Any variable from styles/mkui.css (--mkui-*) may be overridden. Missing keys fall back to the default (dark) values. Switch themes at runtime with appEl.setTheme("solarized").

Standalone mode

<!doctype html>
<link rel="stylesheet" href="/mkui/styles/mkui.css">
<script type="module" src="/mkui/src/index.js"></script>
<mkui-app config="/mkui/client.json"></mkui-app>

Library mode

import { registerPaneType } from "mkui";
import "mkui";   // side-effect: registers custom elements

registerPaneType("clock", (spec, app, host) => {
  const el = document.createElement("div");
  host.appendChild(el);
  setInterval(() => { el.textContent = new Date().toLocaleTimeString(); }, 1000);
});

const root = document.querySelector("mkui-app");
await customElements.whenDefined("mkui-app");
root.setConfig({
  panes:  { clock: { title: "Clock", type: "clock" } },
  frames: [{ id: "f1", x: 0.3, y: 0.3, w: 0.3, h: 0.3,
             layout: { type: "tabs", children: ["clock"] } }],
});

// Add more frames at runtime:
root.workspace.addFrame({ x: 0.5, y: 0.1, w: 0.4, h: 0.4,
                          layout: { type: "tabs", children: ["other-pane"] } });

Built-in widgets and pane types (v1)

  • Widgets (lightweight content inside a pane or statusbar slot):
    • text — static or bind-ed to a state path
    • button — fires an action by name
  • Pane types (whole-pane custom rendering):
    • mkio-table — subscribes to an mkio service (query, subpub, or stream) and renders a live-updating table with flash animations for inserts, deletes, and field changes. Row identity is per-protocol: query uses _mkio_row, stream uses _mkio_ref, subpub uses _mkio_topic. Column headers render immediately when columns is configured; optional labels maps column keys to display text (e.g. { "ts": "Timestamp" }). Subscriptions are deferred until the pane is first visible, unsubscribed immediately when the frame is closed, and dropped after 5 minutes of being hidden (e.g. inactive tab) — brief tab switches preserve the live connection. Large query snapshots render progressively in chunked batches to avoid freezing the UI. Stream tables support paged navigation (prev/next) with a toolbar, and a toggleable "Live" button that switches to real-time streaming (blinking green indicator) — click again to return to paged mode. Stream tables track the last received ref and use it on re-subscribe to avoid duplicate data transfer — existing rows are preserved and only new records are fetched.
  • Dialogs:
    • openDialog(spec, context, app, extra) — config-driven modal dialog with typed fields (text, number, select, checkbox, textarea, readonly, hidden), validation, conditional visibility (showWhen), async service-backed options (optionsFrom), and RPC submission with error handling. A pin button (📌) in the titlebar keeps the dialog open after successful submission — the form resets to defaults only after the server confirms success; errors leave the form intact for retry.
  • Custom pane types are the primary extensibility surface. Register with registerPaneType(name, factory); reference from config as type = "<name>".

Quick start

pip install mkui mkio
mkui init myapp
mkui serve myapp
# http://localhost:8080/

mkui init scaffolds a complete project:

myapp/
  server.toml          ← mkio server config (schema, services, routes)
  config/
    client.toml        ← mkui app config (panes, menus, layout)
  static/
    index.html         ← entry point

mkui serve starts an mkio server that handles static files, config serving (TOML→JSON), the WebSocket endpoint, and the mkio client JS — all from one process, one port.

Installation

pip install mkui mkio

CLI

mkui init [dir]               # scaffold a new project (default: .)
mkui serve [dir] [-p PORT]    # serve with mkio backend (default port from server.toml)
mkui --version

serve reads server.toml in the project directory, resolves the <mkui.static_dir> placeholder to the installed package path, and delegates to mkio.create_app(). The --port flag overrides the port in server.toml.

Library usage

For custom backends (no mkio), serve the static assets directly:

import mkui

# With FastAPI / Starlette:
from starlette.staticfiles import StaticFiles
app.mount("/mkui", StaticFiles(directory=mkui.static_dir))

Running the examples

The standalone and library examples need only a static file server:

cd mkui/static
python3 -m http.server 8000
# http://localhost:8000/examples/standalone-json/
# http://localhost:8000/examples/library-js/

The mkio-table example requires mkio:

cd mkui/static/examples/mkio-table
mkio                # starts on port 8080 (configured in server.toml)
python seed.py      # (optional) populates sample orders in a loop
# http://localhost:8080/

Project layout

mkui/                    Python package (pip install mkui)
  __init__.py            Exposes static_dir path and version
  __main__.py            CLI: init + serve commands
  static/
    src/
      core.js            State store, registries, App class
      index.js           Side-effect entry point
      layout/
        tree.js          Normalized tree math
        drag.js          clampToDock, snap, dropZoneFor, frac↔rect
      components/
        app.js           <mkui-app> — the shell
        menubar.js       <mkui-menubar>
        statusbar.js     <mkui-statusbar>
        workspace.js     <mkui-workspace> — frame list, arrangement, snap
        frame.js         <mkui-frame> + <mkui-pane>
      widgets/
        text.js  button.js  mkio-table.js  mkui-dialog.js
      mkio-bridge.js     Lazy-loads mkio's /mkio.js client
    styles/mkui.css      Default theme (CSS custom properties)
    examples/
      standalone-json/   Loaded from a static config
      library-js/        Built imperatively from JS
      mkio-table/        Live table backed by mkio query/subpub services
pyproject.toml           Python build config
tests/
  layout.test.js         Layout tree unit tests (node:test)
  state.test.js          State + connection lifecycle tests (node:test)
  table.test.js          mkio-table pane tests (node:test)
  dialog.test.js         Dialog expression + submission tests (node:test)
  test_cli.py            CLI init/serve tests (unittest)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mkui-0.1.29.tar.gz (63.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mkui-0.1.29-py3-none-any.whl (63.8 kB view details)

Uploaded Python 3

File details

Details for the file mkui-0.1.29.tar.gz.

File metadata

  • Download URL: mkui-0.1.29.tar.gz
  • Upload date:
  • Size: 63.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mkui-0.1.29.tar.gz
Algorithm Hash digest
SHA256 4342d5c9a721dce46e26547cd92c2e096400926785884337fc4874c5bb311e54
MD5 8dcc6e7eae11834fd63375a2d5bc1466
BLAKE2b-256 2806186c4b29cf98940afca310c273095f430b701ff0abfe93b842d7a255cbb4

See more details on using hashes here.

File details

Details for the file mkui-0.1.29-py3-none-any.whl.

File metadata

  • Download URL: mkui-0.1.29-py3-none-any.whl
  • Upload date:
  • Size: 63.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mkui-0.1.29-py3-none-any.whl
Algorithm Hash digest
SHA256 1981741a917b587809b779c2a30d78591d68bf4b99b32a15a5f6c1c2af7067ae
MD5 c60c2ada2b6ff39a3c32776a47cdd67f
BLAKE2b-256 a4fde4035ef16103aa0826f110536bc0e011aa69450247efccb4e018c828c50d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page