Template Gallery

Jumpstart your widget ideas with production-ready templates bundled in ScriptWidget.

Image

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 

$render(
  <vstack>
    <image id="image" />
  </vstack>
);

Datetime Current

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Clock Template
// 
// Description: Display Clock Time
// 

$render(
  <vstack background="yellow" frame="max,center">
    <date font="title" date="start of today" style="timer" alignment="center" />
  </vstack>
);

Nyan Cat

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Shape Template
// 
// Description: Shape Example
// 



$render(
  <vstack>
    <gif file="cat" />
  </vstack>
);

An Empty Widget

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Empty Template
// 
// Description: Empty script for starter
// 

var text = "Hello ScriptWidget :)";

$render(
  <vstack>
    <text font="caption">{text}</text>
  </vstack>
);

Shape

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Shape Template
// 
// Description: Shape Example
// 


$render(
  <vstack frame="max" animation="clockCustom,20">
    <rect frame="20,60" color="blue" corner="5"></rect>
    <rect frame="20,60" color="red" corner="5"></rect>
  </vstack>
);

Text Today Week

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// 

var d = new Date();

const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
let day = weekday[d.getDay()];


$render(
    <vstack
        background="red"
        frame="max,center"
    >
        <text font="largeTitle" color="black" padding="10">
            {day}
        </text>
    </vstack>
);

Battery & Brightness

Battery state plus brightness (iOS only).

device system
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Battery & Brightness
//

const battery = $device.battery();
const brightness = $system.brightness();
const percent = (battery.level * 100).toFixed(0);

$render(
  <vstack frame="max" padding="12" background="#1e293b">
    <text font="caption" color="#94a3b8">Battery & Brightness</text>
    <text font="title2" color="#e2e8f0">{percent}%</text>
    <text font="caption" color="#94a3b8">State: {battery.state}</text>
    <text font="caption2" color="#64748b">Brightness: {brightness >= 0 ? Math.round(brightness * 100) + "%" : "n/a"}</text>
  </vstack>
);

Daily Quote

Quote of the day from zenquotes.io.

content fetch
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Daily Quote (zenquotes.io)
//

const url = "https://zenquotes.io/api/today";
const result = await fetch(url);
const data = JSON.parse(result);
const quote = data && data.length ? data[0] : { q: "Stay inspired", a: "ScriptWidget" };

$render(
  <vstack frame="max" padding="12" background="#0f172a">
    <text font="caption" color="#94a3b8">Daily Quote</text>
    <text font="caption" color="#e2e8f0">"{quote.q}"</text>
    <text font="caption2" color="#64748b">- {quote.a}</text>
  </vstack>
);

Storage Ring

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Battery Percent Template
// 
// Description: Display system battery percentage
// 

let total = $device.totalDiskSpace();
let free = $device.freeDiskSpace();
let used = total - free;
let percent = used/total;

console.log(`total = ${total}, used = ${used}, percent = ${percent}`);

$render(
  <zstack frame="max" padding="12">
    <circle color="yellow" stroke="20"></circle>
    <circle color="green" stroke="20" trim={1-percent} rotation={90 * 3}></circle>
    <text>{ `${Math.round(percent * 100)}%`} </text>
  </zstack>
);

Image No Margin

ScriptWidget template.

//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//


$render(
  <vstack spacing="0">
    <zstack>
        <image id="image0" padding="0" />
        <text> Hello </text>
    </zstack>
    <zstack>
        <image id="image1" padding="0" />
        <text> World </text>
    </zstack>
    <zstack>
        <image id="image2" padding="0" />
        <text> :) </text>
    </zstack>
  </vstack>
);

System Insights

CPU count, uptime, and memory from $system.

system device
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// System Insights
//

const memory = $system.memory();
const uptimeHours = ($system.systemUptime() / 3600).toFixed(1);
const cpuCount = $system.processorCount();
const activeCpu = $system.activeProcessorCount();

$render(
  <vstack frame="max" padding="12" background="#0f172a">
    <text font="caption" color="#94a3b8">System Insights</text>
    <text font="title3" color="#e2e8f0">{$system.platform().toUpperCase()}</text>
    <text font="caption" color="#94a3b8">OS: {$system.osVersionString()}</text>
    <text font="caption" color="#94a3b8">CPU: {cpuCount} ({activeCpu} active)</text>
    <text font="caption" color="#94a3b8">Memory: {(memory.physical / 1024 / 1024 / 1024).toFixed(1)} GB</text>
    <text font="caption" color="#94a3b8">Uptime: {uptimeHours}h</text>
  </vstack>
);

Text Year Days Left

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Year Days Left Template
// 
// Description: Show you how many days left this year
// 

var today = new Date();
var lastDay = new Date(today.getFullYear(), 12, 31);
var perDay = 1000 * 60 * 60 * 24;
var leftDays = Math.ceil((lastDay.getTime() - today.getTime()) / perDay);

$render(
  <vstack background="red" frame="max">
    <text font="title3" color="white">
      今年还剩
    </text>
    <text font="title" color="white">
      {leftDays} 天
    </text>
  </vstack>
);

Animation Clock

ScriptWidget template.

//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//


$render(
    <vstack frame="max,center">
        <zstack>
            <vstack animation="clockSecond">
                <rect frame="5,50" color="yellow"></rect>
                <rect frame="5,50" color="clear"></rect>
            </vstack>
            <vstack animation="clockMinute">
                <rect frame="5,40" color="blue"></rect>
                <rect frame="5,40" color="clear"></rect>
            </vstack>
            <vstack animation="clockHour">
                <rect frame="5,30" color="red"></rect>
                <rect frame="5,30" color="clear"></rect>
            </vstack>
        </zstack>
    </vstack>
);

Condition Content

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Content Select Template
// 
// Description: Choose content at runtime
// 

var text = "Hello ScriptWidget :)";

var a = (
    <text>a text</text>
)

var b = (
    <text>b text</text>
)

var c = Math.random() * 10 % 10 > 5 ? a : b;


const widget_size = $getenv("widget-size");

$render(
  <vstack>
    <text font="title">{text}</text>
    {c}
    <text font="caption">Widget Size : {widget_size}</text>
  </vstack>
);

GitHub Repo Stats

Stars, forks, and issues for any repo.

developer fetch
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// GitHub Repo Stats
// widget-param: "owner/repo"
//

const repo = ($getenv("widget-param") || "everettjf/ScriptWidget").trim();
const url = `https://api.github.com/repos/${repo}`;
const result = await fetch(url);
const data = JSON.parse(result);

$render(
  <vstack frame="max" padding="12" background="#0f172a">
    <text font="caption" color="#94a3b8">GitHub</text>
    <text font="title3" color="#e2e8f0">{repo}</text>
    <hstack spacing="12">
      <stat title="Stars" value={(data.stargazers_count || 0).toString()} subtitle="" color="#f59e0b" />
      <stat title="Forks" value={(data.forks_count || 0).toString()} subtitle="" color="#38bdf8" />
      <stat title="Issues" value={(data.open_issues_count || 0).toString()} subtitle="" color="#ef4444" />
    </hstack>
  </vstack>
);

Currency Pulse

Exchange rates from open.er-api.com with a configurable base currency.

finance fetch
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Currency Pulse (open.er-api.com)
// widget-param: base currency (optional)
//

const base = ($getenv("widget-param") || "USD").trim().toUpperCase();
const url = `https://open.er-api.com/v6/latest/${base}`;
const result = await fetch(url);
const data = JSON.parse(result);
const rates = data.rates || {};

$render(
  <vstack frame="max" padding="12" background="#0f172a">
    <text font="caption" color="#94a3b8">Currency Pulse</text>
    <text font="title3" color="#e2e8f0">Base: {base}</text>
    <hstack spacing="12">
      <stat title="CNY" value={rates.CNY ? rates.CNY.toFixed(2) : "-"} subtitle="" color="#38bdf8" />
      <stat title="EUR" value={rates.EUR ? rates.EUR.toFixed(2) : "-"} subtitle="" color="#22c55e" />
      <stat title="JPY" value={rates.JPY ? rates.JPY.toFixed(2) : "-"} subtitle="" color="#f59e0b" />
    </hstack>
  </vstack>
);

Image Basic Usage

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Usage for component image
// 

/*

    <image />
    <image systemName="mosaic.fill" />
    <image id="image0" />
    
    <image id="image" mode="fit" ratio="0.6" />
    <image id="image" mode="fill" frame="260,60" />

    <image id="image" mode="fill" clip frame="200,100"/>
    
    <image id="image" mode="fill" clip="rect" frame="200,100"/>
    <image id="image" mode="fill" clip="ellipse" frame="200,100"/>
    <image id="image" mode="fill" clip="circle" frame="200,100"/>
    <image id="image" mode="fill" clip="capsule" frame="200,100"/>

    <image id="image" mode="fill" corner="30" frame="200,100"/>
*/

$render(
  <vstack>
    <image url="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" frame="20,20"/>
    <image id="image" frame="260,60"/>
  </vstack>
);

Live Activity Demo

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Usage for component Live Activity and Dynamic Island
// 


// $render is also for lock screen live activity
$render(
    <vstack frame="max">
        <text>hello live activity</text>
    </vstack>
);


// $dynamic_island is for dynamic island
// on iPhone 14 Pro/ProMax and iOS16.1+
$dynamic_island({
    expanded: { // expanded is required , at least one of the four child below is required
        leading: <text>leading</text>,
        trailing: <text>trailing</text>,
        center: <text>center</text>,
        bottom: <text>bottom</text>,
    },
    compactLeading: <text>compactLeading</text>, // required
    compactTrailing: <text>compactTrailing</text>, // required
    minimal: <text>minimal</text>, // required
});

Crypto Price Ticker

Track real-time crypto prices and 24h change using CoinGecko.

fetch finance
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Crypto Price Ticker (CoinGecko)
// widget-param: coin id, e.g. "bitcoin", "ethereum"
//

const coin = ($getenv("widget-param") || "bitcoin").trim().toLowerCase();
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coin}&vs_currencies=usd&include_24hr_change=true`;
const result = await fetch(url);
const data = JSON.parse(result);
const info = data[coin] || { usd: 0, usd_24h_change: 0 };

const price = info.usd || 0;
const change = info.usd_24h_change || 0;
const changeColor = change >= 0 ? "#22c55e" : "#ef4444";
const changeLabel = change >= 0 ? "+" + change.toFixed(2) : change.toFixed(2);

$render(
  <vstack frame="max" padding="12" background="#0f172a">
    <text font="caption" color="#94a3b8">Crypto Ticker</text>
    <text font="title2" color="#e2e8f0">{coin.toUpperCase()}</text>
    <text font="title3" color="#38bdf8">${price.toFixed(2)}</text>
    <text font="caption" color={changeColor}>{changeLabel}% 24h</text>
  </vstack>
);

Datetime Timezone

ScriptWidget template.

//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//

const widget_size = $getenv("widget-size");
const widget_param = $getenv("widget-param");

const beijingDate = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }).toLocaleString();
const sanJoseDate = new Date().toLocaleString("zh-CN", { timeZone: "America/Los_Angeles" }).toLocaleString();
const newYorkDate = new Date().toLocaleString("zh-CN", { timeZone: "America/New_York" }).toLocaleString();
const sydneyDate = new Date().toLocaleString("zh-CN", { timeZone: "Australia/Sydney" }).toLocaleString();

$render(
    <hstack frame="max">
        <vstack alignment="leading">
            <text font="title3" color="blue" font="custom,Unispace,14">Beijing:</text>
            <text font="title3" color="green" font="custom,Unispace,14">San Jose:</text>
            <text font="title3" color="orange" font="custom,Unispace,14">New York:</text>
            <text font="title3" color="secondary" font="custom,Unispace,14">Sydney:</text>
        </vstack>
        <vstack alignment="leading">
            <text font="title3" color="red" font="custom,Unispace,14">{beijingDate}</text>
            <text font="title3" color="yellow" font="custom,Unispace,14">{sanJoseDate}</text>
            <text font="title3" color="purple" font="custom,Unispace,14">{newYorkDate}</text>
            <text font="title3" color="gray" font="custom,Unispace,14">{sydneyDate}</text>
        </vstack>
    </hstack>
);

Gauge Battery

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Battery Gauge
// 

var percent = $device.battery().level * 100;
percent = percent.toFixed(0);

let gaugeSections = [
  {color: "yellow", value: 0.1},
  {color: "blue", value: 0.2},
  {color: "orange", value: 0.3},
  {color: "green", value: 0.4},
];


$render(
  <vstack frame="max">
    <gauge 
      angle="260" 
      value={percent/100}
      thickness="10" 
      label={percent + "%"} labelFont="caption"
      title="BATTERY" titleFont="caption"
      sections={$json(gaugeSections)}
      >
    </gauge>
  </vstack>
);

Text Days To End Of Month

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 

var d = new Date();
var n = d.getDay();
console.log(n);

let linearGradient = {
  type: "linear",
  colors: ["purple", "white"],
  startPoint: "leading",
  endPoint: "trailing",
};

var a = moment().endOf('month');
var b = moment();
var days = a.diff(b, 'days');

$render(
  <vstack
    background={$gradient(linearGradient)}
    frame="max,center"
  >
    <text font="largeTitle" color="black" padding="10">
        { days + " Days"}
    </text>

    <text font="caption" color="black" padding="0">
        Until end of month
    </text>
  </vstack>
);

Text Days to End Of Year

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 

var d = new Date();
var n = d.getDay();
console.log(n);

let linearGradient = {
  type: "linear",
  colors: ["blue", "white"],
  startPoint: "leading",
  endPoint: "trailing",
};

var a = moment().endOf('year');
var b = moment();
var days = a.diff(b, 'days');

$render(
  <vstack
    background={$gradient(linearGradient)}
    frame="max,center"
  >
    <text font="largeTitle" color="black" padding="10">
        { days + " Days"}
    </text>

    <text font="caption" color="black" padding="0">
        Until end of year
    </text>
  </vstack>
);

Weather

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// 

// https://www.weatherapi.com/
// please register account for your api key
const apikey = "8883e2c78d854356bc813207212502";
const city = "Beijing";
const url = `https://api.weatherapi.com/v1/current.json?key=${apikey}&q=${city}&aqi=no`;

const result = await fetch(url);
console.log(result);
const data = JSON.parse(result);

$render(
  <vstack frame="max" background="#3a86ff">
    <text font="title3" color="white">
      City: {data.location.name}
    </text>
    <text font="title3" color="white">
      Temp: {data.current.temp_c} - {data.current.temp_f}
    </text>
    <text font="title3" color="white">
      Condition: {data.current.condition.text}
    </text>
    <text font="caption2" color="white">
      Updated At: {data.current.last_updated}
    </text>
  </vstack>
);

Device Battery Percent

ScriptWidget template.

//
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Battery Percent Template
// 


var percent = $device.battery().level * 100;
percent = percent.toFixed(0);

let linearGradient = {
  type: "linear",
  colors: ["blue", "red", "green"],
  startPoint: "topLeading",
  endPoint: "bottomTrailing",
};


let radialGradient = {
  type: "radial",
  colors: ["orange", "red", "white"],
  center: "center",
  startRadius: 100,
  endRadius: 470,
};

let angularGradient = {
  type: "angular",
  colors: ["green", "blue", "black", "green", "blue", "black", "green"],
  center: "center",
};

$render(
  <vstack background={$gradient(linearGradient)} frame="max">
    <text font="50">🔋{percent} % </text>
  </vstack>
);

Habit Streak Tracker

Simple streak counter backed by $storage.

storage productivity
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Habit Streak Tracker (uses $storage)
//

const today = new Date();
const todayKey = today.toISOString().slice(0, 10);
const lastDate = $storage.getString("habit.lastDate");
let streakValue = parseInt($storage.getString("habit.streak") || "0", 10);

if (lastDate !== todayKey) {
  if (lastDate) {
    const lastTime = new Date(lastDate + "T00:00:00Z").getTime();
    const diffDays = Math.floor((today.getTime() - lastTime) / (24 * 60 * 60 * 1000));
    if (diffDays === 1) {
      streakValue += 1;
    } else {
      streakValue = 1;
    }
  } else {
    streakValue = 1;
  }
  $storage.setString("habit.lastDate", todayKey);
  $storage.setString("habit.streak", String(streakValue));
}

$render(
  <vstack frame="max" padding="12" background="#0f172a">
    <text font="caption" color="#94a3b8">Habit Streak</text>
    <text font="title2" color="#e2e8f0">{streakValue} days</text>
    <text font="caption2" color="#64748b">Last update: {todayKey}</text>
  </vstack>
);

Focus Countdown

A focus cycle timer with progress gauge.

productivity gauge
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Focus Countdown (25-minute cycle)
//

const cycleMinutes = 25;
const now = new Date();
const minutesInto = now.getMinutes() % cycleMinutes;
const secondsInto = now.getSeconds();
const elapsed = minutesInto * 60 + secondsInto;
const total = cycleMinutes * 60;
const remaining = total - elapsed;
const progress = elapsed / total;

const mm = Math.floor(remaining / 60);
const ss = Math.floor(remaining % 60);
const timeText = `${mm.toString().padStart(2, "0")}:${ss.toString().padStart(2, "0")}`;

$render(
  <vstack frame="max" padding="12" background="#1e293b">
    <text font="caption" color="#94a3b8">Focus Timer</text>
    <text font="title2" color="#e2e8f0">{timeText}</text>
    <gauge
      angle="260"
      value={progress}
      thickness="8"
      label={`${Math.round(progress * 100)}%`}
      labelFont="caption"
      title="Session"
      titleFont="caption"
    />
    <text font="caption2" color="#94a3b8">Cycle: {cycleMinutes} min</text>
  </vstack>
);

Weather Display

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// Weather Template
// 
// Description: Display weather today
// 

// https://www.weatherapi.com/
// please register account for your api key
const apikey = "8883e2c78d854356bc813207212502";
const city = "Beijing";
const url = `https://api.weatherapi.com/v1/current.json?key=${apikey}&q=${city}&aqi=no`;

const result = await fetch(url);
console.log(result);
const data = JSON.parse(result);

$render(
  <vstack frame="max" background="#3a86ff">
    <text font="title3" color="white">
      Weather
    </text>
    <text font="caption" color="white">
      City: {data.location.name}
    </text>
    <text font="caption" color="white">
      Temp: {data.current.temp_c} - {data.current.temp_f}
    </text>
    <text font="caption" color="white">
      Condition: {data.current.condition.text}
    </text>
    <text font="caption2" color="white">
      Updated At: {data.current.last_updated}
    </text>
  </vstack>
);

Check Is Friday Today

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 
// 

var d = new Date();
var n = d.getDay();
console.log(n);

let linearGradient = {
  type: "linear",
  colors: ["yellow", "white"],
  startPoint: "top",
  endPoint: "bottom",
};

$render(
  <vstack
    background={$gradient(linearGradient)}
    frame="max,leading"
    alignment="leading"
  >
    <hstack padding="10">
      <vstack alignment="leading">
        <text font="body" color="black">
          {d.getFullYear()}-{d.getMonth() + 1}-{d.getDate()}
        </text>
        <text font="body" color="black">
          Is Friday today ?
        </text>
      </vstack>
      <spacer />
    </hstack>
    <spacer />
    <text font="largeTitle" color="black" padding="10">
      {n == 5 ? "Yes😊" : "No🤔"}
    </text>
  </vstack>
);

Check Is Working Day Today

ScriptWidget template.

// 
// ScriptWidget 
// https://xnu.app/scriptwidget
// 

var d = new Date();
var n = d.getDay();
console.log(n);

let linearGradient = {
  type: "linear",
  colors: ["yellow", "red"],
  startPoint: "top",
  endPoint: "bottom",
};

$render(
  <vstack
    background={$gradient(linearGradient)}
    frame="max,leading"
    alignment="leading"
  >
    <hstack padding="10">
      <vstack alignment="leading">
        <text font="body" color="black">
          {d.getFullYear()}-{d.getMonth() + 1}-{d.getDate()}
        </text>
        <text font="body" color="black">
          Is Working Day Today ?
        </text>
      </vstack>
      <spacer />
    </hstack>
    <spacer />
    <hstack alignment="center">
        <text font="largeTitle" color="black" padding="10">
        {(n >= 1 && n <= 5) ? "Yes⛽️⛽️⛽️" : "No😄"}
        </text>
    </hstack>    
  </vstack>
);

Location Snapshot

Current coordinates, accuracy, and timestamp from Core Location.

location device
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Location Snapshot
// Requires Location permission in the main app.
//

if (!$location.isAvailable()) {
  $render(
    <vstack frame="max" padding="12" background="#0f172a">
      <text font="title3" color="#f87171">Location Unavailable</text>
      <text font="caption" color="#94a3b8">Location services are disabled.</text>
    </vstack>
  );
} else {
  const status = $location.authorizationStatus();
  const authorized = status === "authorizedWhenInUse" || status === "authorizedAlways";

  if (!authorized) {
    $render(
      <vstack frame="max" padding="12" background="#0f172a">
        <text font="title3" color="#fbbf24">Permission Needed</text>
        <text font="caption" color="#94a3b8">Enable Location access in the app.</text>
      </vstack>
    );
  } else {
    const location = await $location.current();
    const lat = location.latitude.toFixed(4);
    const lon = location.longitude.toFixed(4);
    const accuracy = Math.max(0, Math.round(location.accuracy));

    $render(
      <vstack frame="max" padding="12" background="#111827" spacing="6">
        <text font="caption" color="#94a3b8">Location Snapshot</text>
        <text font="title3" color="#e2e8f0">{lat}, {lon}</text>
        <text font="caption" color="#94a3b8">Accuracy: {accuracy}m</text>
        <text font="caption2" color="#64748b">{location.timestamp}</text>
      </vstack>
    );
  }
}

Stock Snapshot

Daily close and change for a stock symbol via stooq.com.

stocks fetch
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Stock Snapshot (stooq.com)
// widget-param: symbol, e.g. "aapl.us"
//

const symbol = ($getenv("widget-param") || "aapl.us").trim().toLowerCase();
const url = `https://stooq.com/q/l/?s=${symbol}&i=d`;
const result = await fetch(url);
const lines = result.trim().split("\n");
let closeValue = "-";
let openValue = "-";
let dateValue = "-";

if (lines.length > 1) {
  const cols = lines[1].split(",");
  dateValue = cols[1] || "-";
  openValue = cols[2] || "-";
  closeValue = cols[5] || "-";
}

let change = "-";
if (openValue !== "-" && closeValue !== "-") {
  const openNum = parseFloat(openValue);
  const closeNum = parseFloat(closeValue);
  if (!Number.isNaN(openNum) && !Number.isNaN(closeNum)) {
    const diff = closeNum - openNum;
    const pct = (diff / openNum) * 100;
    change = `${diff.toFixed(2)} (${pct.toFixed(2)}%)`;
  }
}

$render(
  <vstack frame="max" padding="12" background="#0f172a">
    <text font="caption" color="#94a3b8">Stock Snapshot</text>
    <text font="title3" color="#e2e8f0">{symbol.toUpperCase()}</text>
    <text font="caption" color="#94a3b8">Close: {closeValue}</text>
    <text font="caption" color="#94a3b8">Change: {change}</text>
    <text font="caption2" color="#64748b">Date: {dateValue}</text>
  </vstack>
);

Sunrise & Sunset

Daily sunrise and sunset for a location.

time location
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Sunrise & Sunset
// widget-param: "lat,lon" (optional)
//

let lat = 37.7749;
let lon = -122.4194;
const param = $getenv("widget-param");
if (param) {
  const parts = param.split(",").map((p) => p.trim());
  if (parts.length >= 2) {
    const latValue = parseFloat(parts[0]);
    const lonValue = parseFloat(parts[1]);
    if (!Number.isNaN(latValue) && !Number.isNaN(lonValue)) {
      lat = latValue;
      lon = lonValue;
    }
  }
}

const url = `https://api.sunrise-sunset.org/json?lat=${lat}&lng=${lon}&formatted=0`;
const result = await fetch(url);
const data = JSON.parse(result);
const sunrise = data.results?.sunrise ? new Date(data.results.sunrise) : null;
const sunset = data.results?.sunset ? new Date(data.results.sunset) : null;

const formatTime = (date) => {
  if (!date) return "-";
  return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
};

$render(
  <vstack frame="max" padding="12" background="#1e293b">
    <text font="caption" color="#94a3b8">Sunrise & Sunset</text>
    <hstack spacing="12">
      <label title={formatTime(sunrise)} systemName="sunrise.fill" color="#f59e0b" />
      <label title={formatTime(sunset)} systemName="sunset.fill" color="#f97316" />
    </hstack>
  </vstack>
);

Meeting Countdown

Countdown to a scheduled time via widget-param.

time schedule
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Meeting Countdown
// widget-param: "2026-02-01 09:30" or ISO string
//

const param = ($getenv("widget-param") || "").trim();
let target = null;

if (param) {
  const normalized = param.includes("T") ? param : param.replace(" ", "T");
  const parsed = new Date(normalized);
  if (!Number.isNaN(parsed.getTime())) {
    target = parsed;
  }
}

if (!target) {
  $render(
    <vstack frame="max" padding="12" background="#0f172a">
      <text font="caption" color="#94a3b8">Meeting Countdown</text>
      <text font="title3" color="#e2e8f0">Set widget-param</text>
      <text font="caption2" color="#64748b">Example: 2026-02-01 09:30</text>
    </vstack>
  );
} else {
  const now = new Date();
  const diffMs = target.getTime() - now.getTime();
  const diffMin = Math.max(0, Math.floor(diffMs / 60000));
  const diffHour = Math.floor(diffMin / 60);
  const diffDay = Math.floor(diffHour / 24);
  const hours = diffHour % 24;
  const minutes = diffMin % 60;

  $render(
    <vstack frame="max" padding="12" background="#1e293b">
      <text font="caption" color="#94a3b8">Next Meeting</text>
      <text font="title2" color="#e2e8f0">{diffDay}d {hours}h {minutes}m</text>
      <text font="caption2" color="#64748b">Target: {target.toLocaleString()}</text>
    </vstack>
  );
}

Air Quality Now

AQI, PM2.5, and PM10 from Open-Meteo. Optional lat/lon widget param.

fetch air-quality
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Air Quality Now - Open-Meteo (no API key)
// widget-param: "lat,lon" (optional)
//

let lat = 37.7749;
let lon = -122.4194;
const param = $getenv("widget-param");
if (param) {
  const parts = param.split(",").map((p) => p.trim());
  if (parts.length >= 2) {
    const latValue = parseFloat(parts[0]);
    const lonValue = parseFloat(parts[1]);
    if (!Number.isNaN(latValue) && !Number.isNaN(lonValue)) {
      lat = latValue;
      lon = lonValue;
    }
  }
}

const url = `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat}&longitude=${lon}&current=pm2_5,pm10,us_aqi&timezone=auto`;
const result = await fetch(url);
const data = JSON.parse(result);
const current = data.current || {};

$render(
  <vstack frame="max" padding="12" background="#0f172a">
    <text font="caption" color="#94a3b8">Air Quality</text>
    <text font="title2" color="#e2e8f0">AQI {current.us_aqi ?? "-"}</text>
    <hstack spacing="12">
      <vstack alignment="leading">
        <text font="caption2" color="#94a3b8">PM2.5</text>
        <text font="caption" color="#e2e8f0">{current.pm2_5 ?? "-"}</text>
      </vstack>
      <vstack alignment="leading">
        <text font="caption2" color="#94a3b8">PM10</text>
        <text font="caption" color="#e2e8f0">{current.pm10 ?? "-"}</text>
      </vstack>
    </hstack>
  </vstack>
);

Local Weather (Location)

Open-Meteo weather using the current device location.

location fetch weather
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Local Weather (Location)
// Requires Location permission in the main app.
//

if (!$location.isAvailable()) {
  $render(
    <vstack frame="max" padding="12" background="#0f172a">
      <text font="title3" color="#f87171">Location Unavailable</text>
      <text font="caption" color="#94a3b8">Location services are disabled.</text>
    </vstack>
  );
  return;
}

const status = $location.authorizationStatus();
const authorized = status === "authorizedWhenInUse" || status === "authorizedAlways";

if (!authorized) {
  $render(
    <vstack frame="max" padding="12" background="#0f172a">
      <text font="title3" color="#fbbf24">Permission Needed</text>
      <text font="caption" color="#94a3b8">Enable Location access in the app.</text>
    </vstack>
  );
  return;
}

const location = await $location.current();
const lat = location.latitude.toFixed(4);
const lon = location.longitude.toFixed(4);

const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,apparent_temperature,weather_code&timezone=auto`;
const result = await fetch(url);
const data = JSON.parse(result);
const current = data.current || {};

$render(
  <vstack frame="max" padding="12" background="#0f172a" spacing="6">
    <text font="caption" color="#94a3b8">Local Weather</text>
    <text font="title2" color="#e2e8f0">{current.temperature_2m ?? "-"} deg C</text>
    <text font="caption" color="#94a3b8">Feels like {current.apparent_temperature ?? "-"} deg C</text>
    <text font="caption2" color="#64748b">Weather code: {current.weather_code ?? "-"}</text>
  </vstack>
);

Location Compass

Compass direction and speed from Core Location.

location device
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Location Compass
// Requires Location permission in the main app.
//

const directionFromCourse = (course) => {
  if (course < 0 || Number.isNaN(course)) return "-";
  const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW", "N"];
  const index = Math.round(course / 45);
  return dirs[index];
};

if (!$location.isAvailable()) {
  $render(
    <vstack frame="max" padding="12" background="#0f172a">
      <text font="title3" color="#f87171">Location Unavailable</text>
      <text font="caption" color="#94a3b8">Location services are disabled.</text>
    </vstack>
  );
  return;
}

const status = $location.authorizationStatus();
const authorized = status === "authorizedWhenInUse" || status === "authorizedAlways";

if (!authorized) {
  $render(
    <vstack frame="max" padding="12" background="#0f172a">
      <text font="title3" color="#fbbf24">Permission Needed</text>
      <text font="caption" color="#94a3b8">Enable Location access in the app.</text>
    </vstack>
  );
  return;
}

const location = await $location.current();
const course = location.course;
const speed = location.speed;

$render(
  <vstack frame="max" padding="12" background="#111827" spacing="6">
    <text font="caption" color="#94a3b8">Compass</text>
    <text font="title2" color="#e2e8f0">{directionFromCourse(course)}</text>
    <text font="caption" color="#94a3b8">Course: {course >= 0 ? Math.round(course) + " deg" : "-"}</text>
    <text font="caption" color="#94a3b8">Speed: {speed >= 0 ? speed.toFixed(1) + " m/s" : "-"}</text>
  </vstack>
);

Health Steps Ring

Shows today's steps with a configurable goal (requires read-only HealthKit authorization).

health gauge
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Health Steps Ring
// Requires read-only HealthKit permission in the main app.
//

const goal = 8000;

if (!$health.isAvailable()) {
  $render(
    <vstack frame="max" padding="12" background="#0f172a">
      <text font="title3" color="#f87171">HealthKit Unavailable</text>
      <text font="caption" color="#94a3b8">Check platform support.</text>
    </vstack>
  );
} else {
  const granted = await $health.requestAuthorization();
  if (!granted) {
    $render(
      <vstack frame="max" padding="12" background="#0f172a">
        <text font="title3" color="#fbbf24">Permission Needed</text>
        <text font="caption" color="#94a3b8">Enable Health access in the app.</text>
      </vstack>
    );
  } else {
    const steps = await $health.stepCountToday();
    const value = steps.value || 0;
    const progress = Math.min(1, value / goal);

    $render(
      <vstack frame="max" padding="12" background="#0f172a">
        <text font="caption" color="#94a3b8">Steps Today</text>
        <gauge
          angle="260"
          value={progress}
          thickness="10"
          label={value.toFixed(0)}
          labelFont="title3"
          title={`Goal ${goal}`}
          titleFont="caption"
        />
        <text font="caption2" color="#94a3b8">{(progress * 100).toFixed(0)}% of goal</text>
      </vstack>
    );
  }
}

System Status Panel

Battery, storage, thermal state, and low-power mode in a compact layout.

system device
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// System Status Panel
//

const battery = $device.battery();
const totalDisk = $device.totalDiskSpace();
const freeDisk = $device.freeDiskSpace();
const usedDisk = Math.max(0, totalDisk - freeDisk);
const diskRatio = totalDisk > 0 ? usedDisk / totalDisk : 0;
const batteryRatio = battery.level || 0;

const gaugeSections = [
  { color: "#22c55e", value: 0.4 },
  { color: "#fbbf24", value: 0.3 },
  { color: "#ef4444", value: 0.3 }
];

$render(
  <vstack frame="max" padding="12" background="#111827">
    <text font="caption" color="#9ca3af">System Status</text>
    <hstack spacing="12">
      <vstack>
        <gauge
          angle="220"
          value={batteryRatio}
          thickness="8"
          label={(batteryRatio * 100).toFixed(0) + "%"}
          labelFont="caption2"
          title="BATTERY"
          titleFont="caption2"
          sections={$json(gaugeSections)}
        />
      </vstack>
      <vstack>
        <gauge
          angle="220"
          value={diskRatio}
          thickness="8"
          label={(diskRatio * 100).toFixed(0) + "%"}
          labelFont="caption2"
          title="STORAGE"
          titleFont="caption2"
          sections={$json(gaugeSections)}
        />
      </vstack>
    </hstack>
    <text font="caption2" color="#9ca3af">Low Power: {$system.lowPowerMode() ? "On" : "Off"}</text>
    <text font="caption2" color="#9ca3af">Thermal: {$system.thermalState()}</text>
  </vstack>
);

Weather Now (Open-Meteo)

Fetch live weather without an API key. Supports lat/lon via widget parameters.

fetch weather
//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Weather Now - Open-Meteo (no API key required)
// widget-param: "lat,lon" (optional)
//

let lat = 37.7749;
let lon = -122.4194;
const param = $getenv("widget-param");
if (param) {
  const parts = param.split(",").map((p) => p.trim());
  if (parts.length >= 2) {
    const latValue = parseFloat(parts[0]);
    const lonValue = parseFloat(parts[1]);
    if (!Number.isNaN(latValue) && !Number.isNaN(lonValue)) {
      lat = latValue;
      lon = lonValue;
    }
  }
}

const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,weather_code,wind_speed_10m&timezone=auto`;
const result = await fetch(url);
const data = JSON.parse(result);

const current = data.current || {};
const units = data.current_units || {};

const weatherMap = {
  0: "Clear",
  1: "Mainly clear",
  2: "Partly cloudy",
  3: "Overcast",
  45: "Fog",
  48: "Fog",
  51: "Drizzle",
  61: "Rain",
  71: "Snow",
  80: "Showers",
  95: "Thunder"
};

const weatherText = weatherMap[current.weather_code] || "Unknown";
const temperature = current.temperature_2m ?? "-";
const wind = current.wind_speed_10m ?? "-";
const time = current.time ?? "";

$render(
  <vstack frame="max" padding="12" background="#0ea5e9">
    <text font="caption" color="#e0f2fe">Weather Now</text>
    <text font="title2" color="white">{temperature}{units.temperature_2m || ""}</text>
    <text font="caption" color="#e0f2fe">{weatherText}</text>
    <text font="caption2" color="#bae6fd">Wind: {wind}{units.wind_speed_10m || ""}</text>
    <text font="caption2" color="#bae6fd">Updated: {time}</text>
  </vstack>
);

Animation Aquarium

ScriptWidget template.

//
// ScriptWidget
// https://xnu.app/scriptwidget
//
// Animation Aquarium
//
// Description: Animation Aquarium
//

/*
🛟
🪼                   🐟🐠             🫧
           🫧                          🫧   🐡
🪼                 🐬       
     🪼                        🦐     🫧
🍀🪸🌿🪸⚓️🗿🐙🐙🌿🪸
*/

let fishHorizontal = {
  type: "swing",
  duration: 20,
  direction: "horizontal", // "horizontal", "vertical"
  distance: 100,
};

let fishVertical = {
  type: "swing",
  duration: 30,
  direction: "vertical", // "horizontal", "vertical"
  distance: 70,
};

let bubbleVertical = {
  type: "swing",
  duration: 15,
  direction: "vertical", // "horizontal", "vertical"
  distance: 50,
};

let linearGradient = {
  type: "linear",
  colors: ["#013A63", "#1E81B0", "#E0FBFC"],
  startPoint: "top",
  endPoint: "bottom",
};
$render(
  <vstack background={$gradient(linearGradient)} frame="max" alignment="top">
    <hstack alignment="leading">
      <text font="body">  🛟</text>
      <spacer />
    </hstack>
    <hstack alignment="leading">
      <text font="body">  🪼        🐟    🐠         🫧</text>
      <text font="body" animation={$animation(fishVertical)}>🐠</text>
      <text font="body">         🫧</text>
      <spacer />
    </hstack>
    <hstack alignment="leading">
      <text font="body">  🫧</text>
      <text font="body">       </text>
      <text font="body" animation={$animation(bubbleVertical)}>🫧</text>
      <text font="body" animation={$animation(fishHorizontal)}>🐡🐡</text>
      <spacer />
    </hstack>
    <hstack alignment="leading">
      <text font="body">  🪼    🫧    🐬       🐬</text>
    </hstack>
    <hstack alignment="leading">
      <text font="body">  🪼   🫧    🦐          🫧🫧</text>
      <spacer />
    </hstack>
    <hstack alignment="leading">
      <text font="body">  🍀 🪸🌿🪸⚓️🗿🐙🐙  🌿🌿 🌿🪸🪸</text>
      <spacer />
    </hstack>
  </vstack>
);

New Episode Tracker

ScriptWidget template.

const API_URL = "https://episodate.com/api/show-details?q="

const colors = {
  primary: "#080808",
  secondary: "green",
  text: {
    primary: "white",
    secondary: "green"
  }
}

const series = [
  "the-lord-of-the-rings",
  "house-of-the-dragon"
]

const fetchSeries = async seriesList => {
  let response = []

  for (let series of seriesList) {
    response.push(
      JSON.parse(
        await fetch(API_URL + series)
      )
    )
  }
  return response
}

const getTimeLeft = airDate => {

  let res
  const millis = airDate - Date.now()
  const secondsLeft = millis / 1000

  const getDays = seconds => {
    return Math.round(seconds / (3600 * 24))
  }

  const getHours = seconds => {
    return Math.round(seconds % (3600 * 24) / 3600)
  }

  const getMinutes = seconds => {
    return Math.round(seconds % 3600 / 60)
  }
  
  if (getDays(secondsLeft) > 0) {
    res = getDays(secondsLeft) + "d "
    res += getHours(secondsLeft) + "h "
    
  } else if (getHours(secondsLeft) > 0) {
    res = getHours(secondsLeft) + " h "
    
  } else {
    res = getMinutes(secondsLeft) + " m "
  }
    
  return res + "left"
}

const Logo = ({logoPath}) => {
   return (
    <zstack>
       <image
         url={logoPath}
         frame="40,40,trailing"
       />
       <rect
         color={colors.secondary}
         stroke="1"
         frame="40,40"
       />
    </zstack>
  )
}

const Entry = ({info}) => {

  const getNextEpisode = countdown => 
    `s${countdown.season}e${countdown.episode}`

  const nextEpisodeRemaining = countdown => {
    const airDate = countdown.air_date
      .replace(" ", "T") + "Z"
    
    return getTimeLeft(new Date(airDate))
  }
  
  return (
    <vstack
      alignment="top" 
    >
      <hstack
        alignment="top" 
      >
        <Logo 
          logoPath={info.image_path}
        />
        <vstack
          alignment="top"
        >
          <text 
            font="14"
            frame="200,15,leading"
            color={colors.text.primary}
          >
            {info.name}
          </text>
          <hstack>
            <text
              font="caption2"
              frame="50,15,leading"
              color={colors.text.secondary}
            >
              {
                info.countdown === null ? "ended"
                : getNextEpisode(info.countdown)
              }
            </text>
            <text
              frame="120,15,trailing"
              font="caption2"
              color={colors.text.secondary}
            >
              {
                info.countdown === null ? ""
                : nextEpisodeRemaining(info.countdown)
              }
            </text>
          </hstack>
        </vstack>
        <spacer/>
      </hstack>
    </vstack>
  )
}

const seriesJson = await fetchSeries(series)

$render(
  <zstack
    background={colors.primary} 
  > 
    <vstack 
      padding="10,10,10,20" 
      frame="max,top"
    >
      {
        seriesJson?.map(series =>
          <Entry
            info={series.tvShow}
          />
        )
      }
    </vstack>
  </zstack>
);

Countdown

ScriptWidget template.

//
// When setting up the widget provide a parameter
// in the setup dialog like Please provide a parameter like
// '2022-11-26 Vacation' if you want to count down
// or up to a date or '2022-11-26T12:35:00 Flight'
// if you want to count up or down to a specific time
// on a date.
//

const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60;
const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;

function is_leap(yr) {
  return yr % 400 === 0 || (yr % 4 === 0 && yr % 100 !== 0);
}

function days_per_month(month, year) {
  if (month === 1) {
    if (is_leap(year)) {
      return 29;
    } else {
      return 28;
    }
  } else {
    months = [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    return months[month];
  }
}

function months_to_days(month) {
  return Math.floor((month * 3057 - 3007) / 100);
}

function years_to_days(yr) {
  return (
    yr * 365 + Math.floor(yr / 4) - Math.floor(yr / 100) + Math.floor(yr / 400)
  );
}

function ymd_to_days(yr, mo, day) {
  scalar = day + months_to_days(mo);
  if (mo > 2)
    // adjust if past February
    scalar -= is_leap(yr) ? 1 : 2;
  yr--;
  scalar += years_to_days(yr);
  return scalar;
}

function determine_sign(remainder, large_unit, small_unit) {
  if (remainder > large_unit - Math.floor(small_unit / 2)) {
    return { text: "≈", count: 1 };
  } else if (remainder > Math.floor(large_unit / 2)) {
    return { text: "<", count: 1 };
  } else if (remainder > Math.floor(small_unit / 2)) {
    return { text: ">", count: 0 };
  } else if (remainder > 0) {
    return { text: "≈", count: 0 };
  } else {
    return { text: "", count: 0 };
  }
}

function Countdown(target, countdown_to) {
  // Convert target and now to tm formats
  const now_date = new Date();
  now_tm = {
    tm_min: countdown_to === "T" ? now_date.getMinutes() : 0,
    tm_hour: countdown_to === "T" ? now_date.getHours() : 0,
    tm_mday: now_date.getDate(),
    tm_mon: now_date.getMonth(),
    tm_year: now_date.getFullYear(),
  };
  const target_date = target;
  target_tm = {
    tm_min: countdown_to === "T" ? target_date.getMinutes() : 0,
    tm_hour: countdown_to === "T" ? target_date.getHours() : 0,
    tm_mday: target_date.getDate(),
    tm_mon: target_date.getMonth(),
    tm_year: target_date.getFullYear(),
  };

  // Choose post-text, max_tm and min_tm
  if (target_date.getTime() > now_date.getTime()) {
    // Count down to
    post_text = "";
    max_tm = target_tm;
    min_tm = now_tm;
  } else {
    // Count down to
    post_text = "ago";
    max_tm = now_tm;
    min_tm = target_tm;
  }

  // Calculate differences in years, months, days, hours and minutes
  received = min_tm.tm_min > max_tm.tm_min ? 60 : 0;
  min_diff = max_tm.tm_min + received - min_tm.tm_min;
  borrow = received > 0 ? 1 : 0;
  received = min_tm.tm_hour + borrow > max_tm.tm_hour ? 24 : 0;
  hour_diff = max_tm.tm_hour + received - min_tm.tm_hour - borrow;
  borrow = received > 0 ? 1 : 0;
  received =
    min_tm.tm_mday + borrow > max_tm.tm_mday
      ? days_per_month(max_tm.tm_mon, max_tm.tm_year)
      : 0;
  day_diff = max_tm.tm_mday + received - min_tm.tm_mday - borrow;
  borrow = received > 0 ? 1 : 0;
  received = min_tm.tm_mon + borrow > max_tm.tm_mon ? 12 : 0;
  month_diff = max_tm.tm_mon + received - min_tm.tm_mon - borrow;
  borrow = received > 0 ? 1 : 0;
  year_diff = max_tm.tm_year - min_tm.tm_year - borrow;

  // Calculate total difference in seconds
  diff =
    ymd_to_days(max_tm.tm_year + 1900, max_tm.tm_mon + 1, max_tm.tm_mday) -
    ymd_to_days(min_tm.tm_year + 1900, min_tm.tm_mon + 1, min_tm.tm_mday);
  if (
    min_tm.tm_hour * 100 + min_tm.tm_min >
    max_tm.tm_hour * 100 + max_tm.tm_min
  )
    diff -= 1;
  diff = diff * 24 + hour_diff;
  diff = diff * 60 + min_diff;
  diff = diff * 60;

  if (diff == 0 || (countdown_to == 'D' && diff == SECONDS_PER_DAY)) {
    // Display one word
    if (diff == 0) {
      if (countdown_to == 'D') {
        return "Today";
      } else {
        return "Now";
      }
    } else {
      if (target_date.getTime() > now_date.getTime()) {
        return "Tomorrow";
      } else {
        return "Yesterday";
      }
    }
  }

  // Display incremental detail
  count = 0;
  remainder = 0;
  if (
    year_diff > 3 ||
    (year_diff == 3 &&
      (month_diff > 0 || day_diff > 0 || hour_diff > 0 || min_diff > 0))
  ) {
    count = year_diff;
    remainder =
      ymd_to_days(
        max_tm.tm_year - year_diff + 1900,
        max_tm.tm_mon + 1,
        max_tm.tm_mday
      ) - ymd_to_days(min_tm.tm_year + 1900, min_tm.tm_mon + 1, min_tm.tm_mday);
    remainder *= SECONDS_PER_DAY;
    remainder += hour_diff * SECONDS_PER_HOUR + min_diff * SECONDS_PER_MINUTE;
    sign = determine_sign(
      remainder,
      (is_leap(max_tm.tm_year + 1900) ? 366 : 365) * SECONDS_PER_DAY,
      SECONDS_PER_DAY
    );
    pre_text = sign.text;
    count += sign.count;
    unit = " years ";
  } else if (
    year_diff * 12 + month_diff > 3 ||
    (month_diff == 3 && (day_diff > 0 || hour_diff > 0 || min_diff > 0))
  ) {
    count = year_diff * 12 + month_diff;
    remainder =
      day_diff * SECONDS_PER_DAY +
      hour_diff * SECONDS_PER_HOUR +
      min_diff * SECONDS_PER_MINUTE;
    sign = determine_sign(
      remainder,
      days_per_month(max_tm.tm_mon, max_tm.tm_year) * SECONDS_PER_DAY,
      SECONDS_PER_DAY
    );
    pre_text = sign.text;
    count += sign.count;
    unit = " months ";
  } else if (diff > 3 * 7 * SECONDS_PER_DAY) {
    count = Math.floor(diff / (7 * SECONDS_PER_DAY));
    remainder = diff % (7 * SECONDS_PER_DAY);
    sign = determine_sign(remainder, 7 * SECONDS_PER_DAY, SECONDS_PER_DAY);
    pre_text = sign.text;
    count += sign.count;
    unit = " weeks ";
  } else if (diff > 3 * SECONDS_PER_DAY || countdown_to === "D") {
    count = Math.floor(diff / SECONDS_PER_DAY);
    remainder = diff % SECONDS_PER_DAY;
    sign = determine_sign(remainder, SECONDS_PER_DAY, SECONDS_PER_HOUR);
    pre_text = sign.text;
    count += sign.count;
    unit = " days ";
  } else if (diff > 3 * SECONDS_PER_HOUR) {
    count = Math.floor(diff / SECONDS_PER_HOUR);
    remainder = diff % SECONDS_PER_HOUR;
    sign = determine_sign(remainder, SECONDS_PER_HOUR, SECONDS_PER_MINUTE);
    pre_text = sign.text;
    count += sign.count;
    unit = " hours ";
  } else if (diff > SECONDS_PER_MINUTE) {
    count = Math.floor(diff / SECONDS_PER_MINUTE);
    pre_text = "";
    unit = " minutes ";
  } else {
    count = 1;
    pre_text = "";
    unit = " minute ";
  }

  return pre_text + count + unit + post_text;
}

// Retrieve target from widget parameter
const param = $getenv("widget-param");
const dtre = /\d\d\d\d\-\d\d\-\d\d(T\d\d\:\d\d\:\d\d)?/;
dt_param = param.match(dtre);
if (!dt_param) {
  $render(
    <vstack frame="max">
      <text>No valid widget parameter specified!</text>

      <text></text>
      <text font="caption">
        Please provide a parameter like '2022-11-26 Vacation' or
        '2022-11-26T12:35:00 Flight'
      </text>
    </vstack>
  );
  return;
} else {
  target = new Date(dt_param[0]);
  event = param.replace(dtre, "").trim();
  countdown_to = dt_param[1] ? "T" : "D";
}

// Update widget
text = Countdown(target, countdown_to);

let linearGradient = {
  type: "linear",
  colors: ["#fb5a72", "#f5243b"],
  startPoint: "top",
  endPoint: "bottom",
};

// Date formatting
if (countdown_to === "T") {
  var dateFormat = {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "numeric",
  };
} else {
  var dateFormat = {
    year: "numeric",
    month: "short",
    day: "numeric",
  };
}

$render(
  <vstack
    background={$gradient(linearGradient)}
    frame="max,leading"
    alignment="leading"
  >
    <hstack padding="10">
      <vstack alignment="leading">
        <text font="body" color="white">
          {event}
        </text>
        <text font="caption" color="white">
          {target.toLocaleDateString(undefined, dateFormat)}
        </text>
      </vstack>
      <spacer />
    </hstack>
    <spacer />
    <text font="title" color="white" padding="10">
      {text}
    </text>
  </vstack>
);

Lunar Date

ScriptWidget template.

//
// ScriptWidget
// https://xnu.app/scriptwidget
//
//

var getLunarData = (function () {
    //公历农历转换
    var calendar = {
        lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
            0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
            0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
            0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
            0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
            0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,
            0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
            0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
            0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
            0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,
            0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
            0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
            0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
            0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
            0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
            0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,
            0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
            0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
            0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
            0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
            0x0d520],
        solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
        Gan: ["\u7532", "\u4e59", "\u4e19", "\u4e01", "\u620a", "\u5df1", "\u5e9a", "\u8f9b", "\u58ec", "\u7678"],
        Zhi: ["\u5b50", "\u4e11", "\u5bc5", "\u536f", "\u8fb0", "\u5df3", "\u5348", "\u672a", "\u7533", "\u9149", "\u620c", "\u4ea5"],
        Animals: ["\u9f20", "\u725b", "\u864e", "\u5154", "\u9f99", "\u86c7", "\u9a6c", "\u7f8a", "\u7334", "\u9e21", "\u72d7", "\u732a"],
        solarTerm: ["\u5c0f\u5bd2", "\u5927\u5bd2", "\u7acb\u6625", "\u96e8\u6c34", "\u60ca\u86f0", "\u6625\u5206", "\u6e05\u660e", "\u8c37\u96e8", "\u7acb\u590f", "\u5c0f\u6ee1", "\u8292\u79cd", "\u590f\u81f3", "\u5c0f\u6691", "\u5927\u6691", "\u7acb\u79cb", "\u5904\u6691", "\u767d\u9732", "\u79cb\u5206", "\u5bd2\u9732", "\u971c\u964d", "\u7acb\u51ac", "\u5c0f\u96ea", "\u5927\u96ea", "\u51ac\u81f3"],
        sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
            '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
            '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
            '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
            'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
            '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
            '97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
            '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
            '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
            '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
            '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
            '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
            '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
            '97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
            '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
            '9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
            '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
            '97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
            '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
            '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
            '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
            '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
            '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
            '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
            '97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
            '97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
            '9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
            '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
            '97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
            '9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
            '7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
            '7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
            '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
            '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
            '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
            '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
            '97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
            '9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
            '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
            '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
            '977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
            '7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
            '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
            '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
            '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
            '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
            '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
            '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
            '977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
            '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
            '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
            '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
            '7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
            '7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
            '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
            '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
            '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
            '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
            '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
            '7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
            '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
            '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
            '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
            '665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
            '7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
            '7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
            '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'],
        nStr1: ["\u65e5", "\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341"],
        nStr2: ["\u521d", "\u5341", "\u5eff", "\u5345"],
        nStr3: ["\u6b63", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341", "\u51ac", "\u814a"],
        lYearDays: function (y) {
            var i, sum = 348;
            for (i = 0x8000; i > 0x8; i >>= 1) { sum += (calendar.lunarInfo[y - 1900] & i) ? 1 : 0; }
            return (sum + calendar.leapDays(y));
        },
        leapMonth: function (y) {
            return (calendar.lunarInfo[y - 1900] & 0xf);
        },
        leapDays: function (y) {
            if (calendar.leapMonth(y)) {
                return ((calendar.lunarInfo[y - 1900] & 0x10000) ? 30 : 29);
            }
            return (0);
        },
        monthDays: function (y, m) {
            if (m > 12 || m < 1) { return -1 }
            return ((calendar.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29);
        },
        solarDays: function (y, m) {
            if (m > 12 || m < 1) { return -1 }
            var ms = m - 1;
            if (ms == 1) {
                return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28);
            } else {
                return (calendar.solarMonth[ms]);
            }
        },
        toGanZhi: function (offset) {
            return (calendar.Gan[offset % 10] + calendar.Zhi[offset % 12]);
        },
        getTerm: function (y, n) {
            if (y < 1900 || y > 2100) { return -1; }
            if (n < 1 || n > 24) { return -1; }
            var _table = calendar.sTermInfo[y - 1900];
            var _info = [
                parseInt('0x' + _table.substr(0, 5)).toString(),
                parseInt('0x' + _table.substr(5, 5)).toString(),
                parseInt('0x' + _table.substr(10, 5)).toString(),
                parseInt('0x' + _table.substr(15, 5)).toString(),
                parseInt('0x' + _table.substr(20, 5)).toString(),
                parseInt('0x' + _table.substr(25, 5)).toString()
            ];
            var _calday = [
                _info[0].substr(0, 1),
                _info[0].substr(1, 2),
                _info[0].substr(3, 1),
                _info[0].substr(4, 2),
                _info[1].substr(0, 1),
                _info[1].substr(1, 2),
                _info[1].substr(3, 1),
                _info[1].substr(4, 2),
                _info[2].substr(0, 1),
                _info[2].substr(1, 2),
                _info[2].substr(3, 1),
                _info[2].substr(4, 2),
                _info[3].substr(0, 1),
                _info[3].substr(1, 2),
                _info[3].substr(3, 1),
                _info[3].substr(4, 2),
                _info[4].substr(0, 1),
                _info[4].substr(1, 2),
                _info[4].substr(3, 1),
                _info[4].substr(4, 2),
                _info[5].substr(0, 1),
                _info[5].substr(1, 2),
                _info[5].substr(3, 1),
                _info[5].substr(4, 2),
            ];
            return parseInt(_calday[n - 1]);
        },
        toChinaMonth: function (m) {
            if (m > 12 || m < 1) { return -1 }
            var s = calendar.nStr3[m - 1];
            s += "\u6708";
            return s;
        },
        toChinaDay: function (d) {
            var s;
            switch (d) {
                case 10:
                    s = '\u521d\u5341';
                    break;
                case 20:
                    s = '\u4e8c\u5341';
                    break;
                case 30:
                    s = '\u4e09\u5341';
                    break;
                default:
                    s = calendar.nStr2[Math.floor(d / 10)];
                    s += calendar.nStr1[d % 10];
            }
            return (s);
        },
        getAnimal: function (y) {
            return calendar.Animals[(y - 4) % 12]
        },
        solar2lunar: function (y, m, d) {
            if (y < 1900 || y > 2100) { return -1; }
            if (y == 1900 && m == 1 && d < 31) { return -1; }
            if (!y) {
                var objDate = new Date();
            } else {
                var objDate = new Date(y, parseInt(m) - 1, d)
            }
            var i, leap = 0, temp = 0;
            var y = objDate.getFullYear(), m = objDate.getMonth() + 1, d = objDate.getDate();
            var offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) / 86400000;
            for (i = 1900; i < 2101 && offset > 0; i++) { temp = calendar.lYearDays(i); offset -= temp; }
            if (offset < 0) { offset += temp; i--; }
            var isTodayObj = new Date(), isToday = false;
            if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) {
                isToday = true;
            }
            var nWeek = objDate.getDay(), cWeek = calendar.nStr1[nWeek];
            if (nWeek == 0) { nWeek = 7; }
            var year = i;
            var leap = calendar.leapMonth(i);
            var isLeap = false;
            for (i = 1; i < 13 && offset > 0; i++) {
                if (leap > 0 && i == (leap + 1) && isLeap == false) {
                    --i;
                    isLeap = true; temp = calendar.leapDays(year);
                } else {
                    temp = calendar.monthDays(year, i);
                }
                if (isLeap == true && i == (leap + 1)) { isLeap = false; }
                offset -= temp;
            }
            if (offset == 0 && leap > 0 && i == leap + 1) {
                if (isLeap) {
                    isLeap = false;
                } else {
                    isLeap = true; --i;
                }
            }
            if (offset < 0) { offset += temp; --i; }
            var month = i;
            var day = offset + 1;
            var sm = m - 1;
            var term3 = calendar.getTerm(year, 3);
            var gzY = calendar.toGanZhi(year - 4);
            gzY = calendar.toGanZhi(year - 4); //modify
            var firstNode = calendar.getTerm(y, (m * 2 - 1));
            var secondNode = calendar.getTerm(y, (m * 2));
            var gzM = calendar.toGanZhi((y - 1900) * 12 + m + 11);
            if (d >= firstNode) {
                gzM = calendar.toGanZhi((y - 1900) * 12 + m + 12);
            }
            var isTerm = false;
            var Term = null;
            if (firstNode == d) {
                isTerm = true;
                Term = calendar.solarTerm[m * 2 - 2];
            }
            if (secondNode == d) {
                isTerm = true;
                Term = calendar.solarTerm[m * 2 - 1];
            }
            var dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10;
            var gzD = calendar.toGanZhi(dayCyclical + d - 1);
            return { 'lYear': year, 'lMonth': month, 'lDay': day, 'Animal': calendar.getAnimal(year), 'IMonthCn': (isLeap ? "\u95f0" : '') + calendar.toChinaMonth(month), 'IDayCn': calendar.toChinaDay(day), 'cYear': y, 'cMonth': m, 'cDay': d, 'gzYear': gzY, 'gzMonth': gzM, 'gzDay': gzD, 'isToday': isToday, 'isLeap': isLeap, 'nWeek': nWeek, 'ncWeek': "\u661f\u671f" + cWeek, 'isTerm': isTerm, 'Term': Term };
        }
    };
    //公历节日
    var _festival1 = {
        '0101': '元旦节',
        '0202': '世界湿地日',
        '0210': '国际气象节',
        '0214': '情人节',
        '0301': '国际海豹日',
        '0303': '全国爱耳日',
        '0305': '学雷锋纪念日',
        '0308': '妇女节',
        '0312': '植树节',
        '0314': '国际警察日',
        '0315': '消费者权益日',
        '0317': '中国国医节 国际航海日',
        '0321': '世界森林日 消除种族歧视国际日 世界儿歌日',
        '0322': '世界水日',
        '0323': '世界气象日',
        '0324': '世界防治结核病日',
        '0325': '全国中小学生安全教育日',
        '0401': '愚人节',
        '0407': '世界卫生日',
        '0422': '世界地球日',
        '0423': '世界图书和版权日',
        '0424': '亚非新闻工作者日',
        '0501': '劳动节',
        '0504': '青年节',
        '0515': '防治碘缺乏病日',
        '0508': '世界红十字日',
        '0512': '国际护士节',
        '0515': '国际家庭日',
        '0517': '世界电信日',
        '0518': '国际博物馆日',
        '0520': '全国学生营养日',
        '0522': '国际生物多样性日',
        '0531': '世界无烟日',
        '0601': '国际儿童节 世界牛奶日',
        '0605': '世界环境日',
        '0606': '全国爱眼日',
        '0617': '防治荒漠化和干旱日',
        '0623': '国际奥林匹克日',
        '0625': '全国土地日',
        '0626': '国际禁毒日',
        '0701': '建党节 香港回归纪念日',
        '0702': '国际体育记者日',
        '0711': '世界人口日 航海日',
        '0801': '建军节',
        '0808': '中国男子节(爸爸节)',
        '0903': '抗日战争胜利纪念日',
        '0908': '国际扫盲日 国际新闻工作者日',
        '0910': '教师节',
        '0916': '国际臭氧层保护日',
        '0918': '九·一八事变纪念日',
        '0920': '国际爱牙日',
        '0927': '世界旅游日',
        '1001': '国庆节 国际音乐日 国际老人节',
        '1002': '国际非暴力日 国际和平与民主自由斗争日',
        '1004': '世界动物日',
        '1006': '老人节',
        '1008': '全国高血压日',
        '1005': '国际教师节',
        '1009': '世界邮政日',
        '1010': '辛亥革命纪念日 世界精神卫生日',
        '1013': '世界保健日 国际减灾日',
        '1014': '世界标准日',
        '1015': '国际盲人节(白手杖节)',
        '1016': '世界粮食日',
        '1017': '世界消除贫困日',
        '1022': '世界传统医药日',
        '1024': '联合国日 世界发展信息日',
        '1031': '世界勤俭日',
        '1107': '十月社会主义革命纪念日',
        '1108': '中国记者日',
        '1109': '全国消防安全宣传教育日',
        '1110': '世界青年节',
        '1111': '国际科学与和平周(本日所属的一周)',
        '1112': '孙中山诞辰纪念日',
        '1114': '联合国糖尿病日',
        '1117': '国际大学生节',
        '1121': '世界问候日 世界电视日',
        '1129': '国际声援巴勒斯坦人民国际日',
        '1201': '世界艾滋病日',
        '1203': '世界残疾人日',
        '1204': '宪法日',
        '1205': '国际志愿人员日',
        '1209': '世界足球日',
        '1210': '世界人权日',
        '1212': '西安事变纪念日',
        '1213': '南京大屠杀纪念日',
        '1220': '澳门回归纪念',
        '1221': '国际篮球日',
        '1224': '平安夜',
        '1225': '圣诞节',
        '1226': '毛泽东诞辰纪念日'
    };
    //某月的第几个星期几,第3位为5表示最后一星期
    var _festival2 = {
        '0110': '黑人日',
        '0150': '世界麻风日',
        '0440': '世界儿童日',
        '0520': '国际母亲节',
        '0532': '国际牛奶日',
        '0530': '全国助残日',
        '0630': '父亲节',
        '0711': '世界建筑日',
        '0730': '被奴役国家周',
        '0936': '世界清洁地球日',
        '0932': '国际和平日',
        '0940': '国际聋人节',
        '1011': '国际住房日',
        '1024': '世界视觉日',
        '1144': '感恩节',
        '1220': '国际儿童电视广播日'
    };
    //农历节日
    var _festival3 = {
        '0101': '春节',
        '0102': '初二',
        '0103': '初三',
        '0115': '元宵节',
        '0202': '龙抬头节',
        '0323': '妈祖生辰',
        '0505': '端午节',
        '0707': '七夕节',
        '0715': '中元节',
        '0815': '中秋节',
        '0909': '重阳节',
        '1208': '腊八节',
        '1223': '小年',
        '0100': '除夕'
    };
    //假日安排数据
    var _holiday = {
        '2011': { '0402': 0, '0403': 1, '0404': 1, '0405': 1, '0430': 1, '0501': 1, '0502': 1, '0604': 1, '0605': 1, '0606': 1, '0910': 1, '0911': 1, '0912': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 0, '1009': 0, '1231': 0 },
        '2012': {
            '0101': 1, '0102': 1, '0103': 1, '0121': 0, '0122': 1, '0123': 1, '0124': 1, '0125': 1, '0126': 1, '0127': 1, '0128': 1, '0129': 0, '0331': 0, '0401'
                : 0, '0402': 1, '0403': 1, '0404': 1, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0622': 1, '0623': 1, '0624': 1, '0929': 0, '0930': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1
        },
        '2013': { '0101': 1, '0102': 1, '0103': 1, '0105': 0, '0106': 0, '0209': 1, '0210': 1, '0211': 1, '0212': 1, '0213': 1, '0214': 1, '0215': 1, '0216': 0, '0217': 0, '0404': 1, '0405': 1, '0406': 1, '0407': 0, '0427': 0, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0608': 0, '0609': 0, '0610': 1, '0611': 1, '0612': 1, '0919': 1, '0920': 1, '0921': 1, '0922': 0, '0929': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1012': 0 },
        '2014': { '0101': 1, '0126': 0, '0131': 1, '0201': 1, '0202': 1, '0203': 1, '0203': 1, '0204': 1, '0205': 1, '0206': 1, '0208': 0, '0405': 1, '0406': 1, '0407': 1, '0501': 1, '0502': 1, '0503': 1, '0504': 0, '0531': 1, '0601': 1, '0602': 1, '0908': 1, '0928': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1011': 0 },
        '2015': { '0101': 1, '0102': 1, '0103': 1, '0104': 0, '0215': 0, '0218': 1, '0219': 1, '0220': 1, '0221': 1, '0222': 1, '0223': 1, '0224': 1, '0228': 0, '0404': 1, '0405': 1, '0406': 1, '0501': 1, '0502': 1, '0503': 1, '0620': 1, '0621': 1, '0622': 1, '0903': 1, '0904': 1, '0905': 1, '0906': 0, '0927': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1010': 0 },
        '2016': { '0101': 1, '0102': 1, '0103': 1, '0206': 0, '0207': 1, '0208': 1, '0209': 1, '0210': 1, '0211': 1, '0212': 1, '0213': 1, '0214': 0, '0402': 1, '0403': 1, '0404': 1, '0430': 1, '0501': 1, '0502': 1, '0609': 1, '0610': 1, '0611': 1, '0612': 0, '0915': 1, '0916': 1, '0917': 1, '0918': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 0, '1009': 0 },
        '2017': { '0101': 1, '0102': 1, '0122': 0, '0127': 1, '0128': 1, '0129': 1, '0130': 1, '0131': 1, '0201': 1, '0202': 1, '0204': 0, '0401': 0, '0402': 1, '0403': 1, '0404': 1, '0429': 1, '0430': 1, '0501': 1, '0527': 0, '0528': 1, '0529': 1, '0530': 1, '0930': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 1, '1230': 1, '1231': 1 },
        '2018': { '0101': 1, '0211': 0, '0215': 1, '0216': 1, '0217': 1, '0218': 1, '0219': 1, '0220': 1, '0221': 1, '0224': 0, '0405': 1, '0406': 1, '0407': 1, '0408': 0, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0616': 1, '0617': 1, '0618': 1, '0922': 1, '0923': 1, '0924': 1, '0929': 0, '0930': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1229': 0, '1230': 1, '1231': 1 },
        '2019': { '0101': 1, '0202': 0, '0203': 0, '0204': 1, '0205': 1, '0206': 1, '0207': 1, '0208': 1, '0209': 1, '0210': 1, '0405': 1, '0406': 1, '0407': 1, '0428': 0, '0501': 1, '0502': 1, '0503': 1, '0504': 1, '0505': 0, '0607': 1, '0608': 1, '0609': 1, '0913': 1, '0914': 1, '0915': 1, '0929': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1012': 0 }
    };
    //获取日期数据
    var getDateObj = function (year, month, day) {
        var date = arguments.length && year ? new Date(year, month - 1, day) : new Date();
        return {
            'year': date.getFullYear(),
            'month': date.getMonth() + 1,
            'day': date.getDate(),
            'week': date.getDay()
        };
    };
    //当天
    var _today = getDateObj();
    //获取当月天数
    var getMonthDays = function (obj) {
        var day = new Date(obj.year, obj.month, 0);
        return day.getDate();
    };
    if (!String.prototype.trim) {
        String.prototype.trim = function () {
            return this.replace(/^\s+|\s+$/g, '');
        };
    }
    //获取某天日期信息
    var getDateInfo = function (obj) {
        var info = calendar.solar2lunar(obj.year, obj.month, obj.day);
        var cMonth = info.cMonth > 9 ? '' + info.cMonth : '0' + info.cMonth;
        var cDay = info.cDay > 9 ? '' + info.cDay : '0' + info.cDay;
        var lMonth = info.lMonth > 9 ? '' + info.lMonth : '0' + info.lMonth;
        var lDay = info.lDay > 9 ? '' + info.lDay : '0' + info.lDay;
        var code1 = cMonth + cDay;
        var code2 = cMonth + Math.ceil(info.cDay / 7) + info.nWeek % 7;
        var code3 = lMonth + lDay;
        var days = getMonthDays(obj);
        //节日信息
        info['festival'] = '';
        if (_festival3[code3]) {
            info['festival'] += _festival3[code3];
        }
        if (_festival1[code1]) {
            info['festival'] += ' ' + _festival1[code1];
        }
        if (_festival2[code2]) {
            info['festival'] += ' ' + _festival2[code2];
        }
        if (obj['day'] + 7 > days) {
            var code4 = cMonth + 5 + info.nWeek % 7;
            if (code4 != code2 && _festival2[code4]) {
                info['festival'] += ' ' + _festival2[code4];
            }
        }
        info['festival'] = info['festival'].trim();
        //放假、调休等标记
        info['sign'] = '';
        if (_holiday[info.cYear]) {
            var holiday = _holiday[info.cYear];
            if (typeof holiday[code1] != 'undefined') {
                info['sign'] = holiday[code1] ? 'holiday' : 'work';
            }
        }
        if (info.cYear == _today.year && info.cMonth == _today.month && info.cDay == _today.day) {
            info['sign'] = 'today';
        }
        return info;
    };
    //获取日历信息
    return (function (date) {
        var date = date || _today;
        var first = getDateObj(date['year'], date['month'], 1);		//当月第一天
        var days = getMonthDays(date);							//当月天数
        var data = [];										//日历信息
        var obj = {};
        //上月日期
        for (var i = first['week']; i > 0; i--) {
            obj = getDateObj(first['year'], first['month'], first['day'] - i);
            var info = getDateInfo(obj);
            info['disabled'] = 1;
            data.push(info);
        }
        //当月日期
        for (var i = 0; i < days; i++) {
            obj = {
                'year': first['year'],
                'month': first['month'],
                'day': first['day'] + i,
                'week': (first['week'] + i) % 7
            };
            var info = getDateInfo(obj);
            info['disabled'] = 0;
            data.push(info);
        }
        //下月日期
        var last = obj;
        for (var i = 1; last['week'] + i < 7; i++) {
            obj = getDateObj(last['year'], last['month'], last['day'] + i);
            var info = getDateInfo(obj);
            info['disabled'] = 1;
            data.push(info);
        }
        return {
            'date': getDateInfo(date),				//当前日历选中日期
            'data': data
        };
    });
})();



var d = new Date();

var lunarInfo = getLunarData({
    'year': d.getFullYear(),
    'month': d.getMonth() + 1,
    'day': d.getDate()
});

// console.log(JSON.stringify(lunarInfo, 2));

/*
"date": {
    "lYear": 2022,
    "lMonth": 1,
    "lDay": 14,
    "Animal": "虎",
    "IMonthCn": "正月",
    "IDayCn": "十四",
    "cYear": 2022,
    "cMonth": 2,
    "cDay": 14,
    "gzYear": "壬寅",
    "gzMonth": "壬寅",
    "gzDay": "戊戌",
    "isToday": true,
    "isLeap": false,
    "nWeek": 1,
    "ncWeek": "星期一",
    "isTerm": false,
    "Term": null,
    "festival": "情人节",
    "sign": "today"
  },

*/

let today = lunarInfo.date;

let linearGradient = {
    type: "linear",
    colors: ["red", "yellow"],
    startPoint: "top",
    endPoint: "bottom",
};

$render(
    <vstack
        background={$gradient(linearGradient)}
        frame="max,center"
    >
        <hstack>
            <text font="title2">{today.Animal} 年</text>

            <vstack>
                <text font="title">{today.IMonthCn}</text>
                <text font="title">{today.IDayCn}</text>
            </vstack>
        </hstack>

        <text font="caption">{today.festival}</text>

    </vstack>
);
Templates live in Shared/ScriptWidgetRuntime/Resource/Script.bundle/template/ and can be imported directly into the app.