Skip to main content
The Scriptable app lets you run JavaScript directly on iOS and iPadOS, making it a perfect host for a Wrixton home-screen widget. The included widget/track.js script calls the /api/widget endpoint, hydrates your today/focus list, and renders each item with a priority dot — all without installing anything beyond Scriptable itself. Tapping the widget anywhere opens the full web app.

Requirements

  • Scriptable — free on the App Store, works on iPhone and iPad running iOS/iPadOS 14 or later.
  • A Wrixton account with a valid API key (see track config in the CLI or the Settings page in the web app).

Setup

1

Install Scriptable from the App Store

Search for Scriptable on the App Store and install it. No account or subscription is required.
2

Open the widget script

The script lives at widget/track.js in the project repository. You can open the file on your Mac and AirDrop it to your device, or simply copy the full source from the code block below.
3

Create a new script in Scriptable

Open Scriptable, tap + in the top-right corner, and paste in the entire contents of track.js. Give the script a recognisable name — Wrixton works well — then tap Done.
4

Edit the two config lines at the top

At the very top of the script you will find:
const APP_URL = "https://projects.wrixton.xyz";
const API_KEY = "your-personal-track-key";
Leave APP_URL as-is unless you are running a self-hosted deployment. Replace "your-personal-track-key" with your actual API key. You can find it in ~/.track/config.toml on any machine where you have run track config.
5

Add a Scriptable widget to your home screen

Long-press any empty area of your home screen to enter jiggle mode, tap +, search for Scriptable, and choose a widget size. In the widget configuration sheet, set Script to the name you gave the script in step 3, and set When Interacting to Open URL (the script sets this automatically to APP_URL). Tap Done — your Wrixton widget is live.

Widget script

The script is self-contained and uses only Scriptable’s built-in APIs — no third-party packages required.
// --- CONFIGURATION ---
const APP_URL = "https://projects.wrixton.xyz";
const API_KEY = "your-personal-track-key"; // Get this from your CLI config
// ---------------------

const widget = new ListWidget();

try {
  const url = `${APP_URL}/api/widget`;
  const req = new Request(url);
  req.headers = {
    "x-track-key": API_KEY
  };

  const data = await req.loadJSON();
  
  if (data.error) {
    throw new Error(data.error);
  }

  const items = data.items || [];
  const theme = data.theme || {
    paper: "#f6f3ec",
    ink: "#16140f",
    accent: "#a9461a",
    muted: "#8a857b",
    rule: "#e5dfd0",
    ruleSoft: "#efe9da"
  };

  const paper = new Color(theme.paper);
  const ink = new Color(theme.ink);
  const accent = new Color(theme.accent);
  const muted = new Color(theme.muted);
  const rule = new Color(theme.rule);
  const ruleSoft = new Color(theme.ruleSoft);

  widget.backgroundColor = paper;
  widget.url = APP_URL;

  // const maxItems = config.widgetFamily === "medium" ? 8 : 5;
  const maxItems = 10;

  // Header
  let titleStack = widget.addStack();
  titleStack.centerAlignContent();
  
  let title = titleStack.addText("Project & Todo Tracker");
  title.font = Font.italicSystemFont(18); // Mimic Fraunces
  title.textColor = ink;
  title.lineLimit = 1;
  title.minimumScaleFactor = 0.6; // long name; shrink to fit small widget
  
  titleStack.addSpacer();
  
  let now = new Date();
  let hours = now.getHours();
  const ampm = hours >= 12 ? "PM" : "AM";
  hours = hours % 12 || 12;
  let time = titleStack.addText(`${hours}:${now.getMinutes().toString().padStart(2, '0')} ${ampm}`);
  time.font = Font.mediumSystemFont(10);
  time.textColor = muted;

  widget.addSpacer(10);

  if (items.length === 0) {
    let msg = widget.addText("No tasks today.");
    msg.font = Font.italicSystemFont(14);
    msg.textColor = muted;
    msg.centerAlignText();
  } else {
    for (const item of items.slice(0, maxItems)) {
      let row = widget.addStack();
      row.centerAlignContent();
      row.size = new Size(0, 22);
      
      // Priority Dot or Icon
      let dotStack = row.addStack();
      dotStack.size = new Size(12, 12);
      dotStack.centerAlignContent();
      
      let dot = dotStack.addText("●");
      dot.font = Font.systemFont(8);
      
      if (item.priority === 1) {
        dot.textColor = accent;
      } else if (item.kind === "grocery") {
        dot.textColor = muted;
        dot.text = "○";
      } else {
        dot.textColor = new Color("#3a352e"); // ink-2
      }
      
      row.addSpacer(8);

      let rowTxt = row.addText(item.title);
      rowTxt.font = Font.systemFont(13);
      rowTxt.textColor = ink;
      rowTxt.lineLimit = 1;
      
      row.addSpacer();
      
      widget.addSpacer(2);
      
      // Divider line (simulated)
      let line = widget.addStack();
      line.backgroundColor = ruleSoft;
      line.size = new Size(0, 0.5);
      
      widget.addSpacer(2);
    }
    
    const total = data.total || items.length;
    if (total > maxItems) {
      widget.addSpacer(2);
      let more = widget.addText(`+ ${total - maxItems} more`);
      more.font = Font.systemFont(10);
      more.textColor = muted;
    }
  }
} catch (e) {
  let errTxt = widget.addText("Update failed");
  errTxt.font = Font.systemFont(10);
  errTxt.textColor = new Color("#a83838"); // overdue
  console.log(e);
}

widget.addSpacer();
Script.setWidget(widget);
Script.complete();

How the widget renders items

The widget calls /api/widget with your x-track-key header and receives your today/focus list — the same items shown by track today or the web app’s Today lens. Each item row displays:
  • Priority dot — filled accent-colour dot () for priority 1 items, an open circle () for groceries, and a dark dot for everything else.
  • Title — truncated to one line if it is too long for the widget width.
  • A thin hairline rule separates each row.
The header always shows the current time in 12-hour format alongside the app name.

Adjusting the item limit

The maxItems constant near the top of the script controls how many rows the widget shows before displaying a + N more footer:
const maxItems = 10;
Lower this value if your widget feels cramped, or raise it if you prefer a denser list. The commented-out line above it shows how you could set different limits per widget size (small, medium, large) using config.widgetFamily.
If the widget shows “Update failed”, double-check that your API_KEY value matches the key in ~/.track/config.toml exactly — including any leading characters. You can also run the script interactively inside Scriptable to see the full error in the console.

Auto-refresh and tap behaviour

Scriptable widgets refresh on iOS’s own schedule (roughly every 15–30 minutes depending on battery and usage patterns — this is an OS constraint, not a Scriptable one). Tapping anywhere on the widget opens the Wrixton web app at APP_URL in your default browser.