All widgets
Weather
Comes with Chopstick
The weather where you are on the island (now, today's range and the next days), refreshed every 15 minutes; ask for another city to look it up.
Already installed? It's in your widget library.










Type it in the notch's search
- Weather in city
- Show weather
- Refresh weather
What it runs
A widget is a folder of plain files. Here is all of its code.
widget.json
{
"id": "weather",
"name": "Weather",
"icon": "icon.png",
"description": "The weather where you are on the island (now, today's range and the next days), refreshed every 15 minutes; ask for another city to look it up.",
"ambient": true,
"commands": {
"city": {
"phrase": "Weather in {city}",
"description": "Show the weather in a city.",
"inputs": {
"city": {
"type": "string",
"description": "City name, e.g. Lisbon"
}
},
"preview": {
"state": "lookup",
"run": "fetch.sh"
},
"do": [
{
"set": {
"city": "{{input.city}}"
}
},
{
"go": "loading"
}
]
},
"show": {
"description": "Show the weather where you are.",
"phrase": "Show weather",
"do": [
{
"go": "now"
}
]
},
"refresh": {
"description": "Fetch the weather again.",
"phrase": "Refresh weather",
"do": [
{
"refresh": "here"
}
]
}
},
"states": {
"now": {
"compact": {
"leading": {
"symbol": "{{here.symbol | cloud.sun.fill}}",
"tint": "{{here.tint}}"
},
"trailing": {
"text": "{{here.temp | –}}"
}
},
"expanded": {
"size": "regular",
"header": {
"symbol": "location.fill",
"title": "{{here.city | Weather}}"
},
"blocks": [
{
"row": [
{
"stack": [
{
"text": "{{here.temp}} {{here.summary}}",
"style": "title"
},
{
"text": "H {{here.high}} · L {{here.low}} · Feels {{here.feels}}",
"style": "caption"
}
],
"align": "leading",
"spacing": "tight"
},
"spacer",
{
"stack": [
{
"note": "Humidity {{here.humidity}}"
},
{
"note": "Wind {{here.wind}}"
}
],
"align": "trailing",
"spacing": "tight"
}
],
"align": "center"
},
{
"list": {
"from": "here.days",
"limit": 5,
"symbol": "{{item.symbol}}",
"title": "{{item.name}} {{item.range}}",
"subtitle": "{{item.summary}}"
}
}
]
},
"keys": [
{
"key": "cmd+r",
"label": "Refresh",
"do": [
{
"refresh": "here"
}
]
}
]
},
"loading": {
"task": true,
"compact": {
"leading": {
"symbol": "cloud.sun.fill"
},
"trailing": "spinner"
},
"on_enter": [
{
"run": "fetch.sh",
"then": "ready",
"else": "failed"
}
],
"keys": [
{
"key": "esc",
"do": [
"dismiss"
]
}
]
},
"ready": {
"task": true,
"open": true,
"dismiss_after": 10,
"compact": {
"leading": {
"symbol": "{{result.symbol}}",
"tint": "{{result.tint}}"
},
"trailing": {
"text": "{{result.temp}}"
}
},
"expanded": {
"size": "regular",
"header": {
"symbol": "location.fill",
"title": "{{result.city}}"
},
"blocks": [
{
"row": [
{
"stack": [
{
"text": "{{result.temp}} {{result.summary}}",
"style": "title"
},
{
"text": "H {{result.high}} · L {{result.low}} · Feels {{result.feels}}",
"style": "caption"
}
],
"align": "leading",
"spacing": "tight"
},
"spacer",
{
"stack": [
{
"note": "Humidity {{result.humidity}}"
},
{
"note": "Wind {{result.wind}}"
}
],
"align": "trailing",
"spacing": "tight"
}
],
"align": "center"
},
{
"list": {
"from": "result.days",
"limit": 5,
"symbol": "{{item.symbol}}",
"title": "{{item.name}} {{item.range}}",
"subtitle": "{{item.summary}}"
}
}
]
},
"keys": [
{
"key": "r",
"label": "Refresh",
"do": [
{
"go": "loading"
}
]
},
{
"key": "esc",
"do": [
"dismiss"
]
}
]
},
"lookup": {
"task": true,
"open": true,
"compact": {
"leading": {
"symbol": "{{result.symbol | cloud.sun.fill}}",
"tint": "{{result.tint}}"
},
"trailing": {
"text": "{{result.temp}}"
}
},
"expanded": {
"size": "regular",
"header": {
"symbol": "location.fill",
"title": "{{result.city | Weather}}"
},
"blocks": [
{
"text": "{{result.city | Looking it up…}}",
"style": "caption"
},
{
"text": "{{result.headline | …}}",
"style": "title"
},
{
"list": {
"from": "result.days",
"limit": 3,
"symbol": "{{item.symbol}}",
"title": "{{item.name}} {{item.range}}",
"subtitle": "{{item.summary}}"
}
},
{
"note": "{{error}}"
},
{
"input": {
"placeholder": "Another city…",
"value": "{{input.city}}",
"do": [
{
"command": "city",
"with": {
"city": "{{item.text}}"
}
}
]
}
}
]
},
"keys": [
{
"key": "esc",
"do": [
"dismiss"
]
}
]
},
"failed": {
"task": true,
"open": true,
"dismiss_after": 6,
"compact": {
"leading": {
"symbol": "exclamationmark.triangle",
"tint": "orange"
}
},
"expanded": {
"size": "compact",
"header": {
"symbol": "cloud.sun.fill",
"title": "Weather"
},
"blocks": [
{
"text": "{{error | Couldn't load the weather}}",
"style": "body",
"lines": 3
}
]
},
"keys": [
{
"key": "r",
"label": "Retry",
"do": [
{
"go": "loading"
}
]
},
{
"key": "esc",
"do": [
"dismiss"
]
}
]
}
},
"preview": {
"states": [
"now",
"loading",
"ready",
"failed",
"lookup"
],
"data": {
"error": "Couldn't reach the weather service",
"result": {
"city": "Berlin",
"temp": "14°",
"feels": "12°",
"humidity": "72%",
"wind": "11 km/h",
"symbol": "cloud.sun.fill",
"summary": "Partly cloudy",
"tint": "yellow",
"high": "16°",
"low": "8°",
"days": [
{
"name": "Today",
"symbol": "cloud.sun.fill",
"summary": "Partly cloudy",
"range": "8° / 16°"
},
{
"name": "Mon",
"symbol": "cloud.rain.fill",
"summary": "Rain",
"range": "9° / 13°"
},
{
"name": "Tue",
"symbol": "cloud.fill",
"summary": "Overcast",
"range": "7° / 12°"
},
{
"name": "Wed",
"symbol": "sun.max.fill",
"summary": "Clear",
"range": "6° / 15°"
},
{
"name": "Thu",
"symbol": "cloud.drizzle.fill",
"summary": "Drizzle",
"range": "8° / 14°"
}
],
"headline": "14° Partly cloudy"
},
"here": {
"city": "Berlin",
"temp": "14°",
"feels": "12°",
"humidity": "72%",
"wind": "11 km/h",
"symbol": "cloud.sun.fill",
"summary": "Partly cloudy",
"tint": "yellow",
"high": "16°",
"low": "8°",
"days": [
{
"name": "Today",
"symbol": "cloud.sun.fill",
"summary": "Partly cloudy",
"range": "8° / 16°"
},
{
"name": "Mon",
"symbol": "cloud.rain.fill",
"summary": "Rain",
"range": "9° / 13°"
},
{
"name": "Tue",
"symbol": "cloud.fill",
"summary": "Overcast",
"range": "7° / 12°"
},
{
"name": "Wed",
"symbol": "sun.max.fill",
"summary": "Clear",
"range": "6° / 15°"
},
{
"name": "Thu",
"symbol": "cloud.drizzle.fill",
"summary": "Drizzle",
"range": "8° / 14°"
}
]
},
"input": {
"city": "Berlin"
}
}
},
"initial": "now",
"watch": [
{
"script": "fetch.sh",
"every": 900,
"as": "here"
}
]
}
fetch.sh
#!/bin/sh
# Finds the location by IP (ipinfo.io), gets the weather from Open-Meteo (no key), prints one JSON object.
# With a city (the city command sets it), looks it up instead (Open-Meteo geocoding).
if [ -n "$CHOPSTICK_CITY" ]; then
loc=$(curl -fsS --max-time 8 -G https://geocoding-api.open-meteo.com/v1/search --data-urlencode "name=$CHOPSTICK_CITY" -d count=1) \
|| { echo "Couldn't look up $CHOPSTICK_CITY" >&2; exit 1; }
lat=$(printf '%s' "$loc" | sed -n 's/.*"latitude": *\([-0-9.]*\).*/\1/p')
lon=$(printf '%s' "$loc" | sed -n 's/.*"longitude": *\([-0-9.]*\).*/\1/p')
[ -n "$lat" ] || { echo "Couldn't find a city called $CHOPSTICK_CITY" >&2; exit 1; }
else
loc=$(curl -fsS --max-time 8 https://ipinfo.io/json) || { echo "Couldn't find your location" >&2; exit 1; }
latlon=$(printf '%s' "$loc" | sed -n 's/.*"loc": *"\([^"]*\)".*/\1/p')
lat=${latlon%,*}
lon=${latlon#*,}
[ -n "$lat" ] || { echo "Couldn't find your location" >&2; exit 1; }
fi
wx=$(curl -fsS --max-time 8 "https://api.open-meteo.com/v1/forecast?latitude=$lat&longitude=$lon¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code,is_day&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=5") \
|| { echo "Couldn't reach the weather service" >&2; exit 1; }
osascript -l JavaScript - "$loc" "$wx" <<'JS'
function run(argv) {
const loc = JSON.parse(argv[0]), wx = JSON.parse(argv[1]);
const c = wx.current, d = wx.daily, day = c.is_day === 1;
function info(code, isDay) {
if (code === 0) return [isDay ? "sun.max.fill" : "moon.stars.fill", "Clear", isDay ? "yellow" : "cyan"];
if (code <= 2) return [isDay ? "cloud.sun.fill" : "cloud.moon.fill", "Partly cloudy", "yellow"];
if (code === 3) return ["cloud.fill", "Overcast", "gray"];
if (code <= 48) return ["cloud.fog.fill", "Fog", "gray"];
if (code <= 57) return ["cloud.drizzle.fill", "Drizzle", "cyan"];
if (code <= 67) return ["cloud.rain.fill", "Rain", "blue"];
if (code <= 77) return ["cloud.snow.fill", "Snow", "cyan"];
if (code <= 82) return ["cloud.heavyrain.fill", "Showers", "blue"];
if (code <= 86) return ["cloud.snow.fill", "Snow showers", "cyan"];
return ["cloud.bolt.rain.fill", "Thunderstorm", "purple"];
}
const r = n => Math.round(n) + "°";
const now = info(c.weather_code, day);
const names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const days = d.time.map((t, i) => {
const w = info(d.weather_code[i], true);
return {
name: i === 0 ? "Today" : names[new Date(t + "T12:00:00").getDay()],
symbol: w[0], summary: w[1],
range: r(d.temperature_2m_min[i]) + " / " + r(d.temperature_2m_max[i])
};
});
return JSON.stringify({
city: loc.city || (loc.results && loc.results[0].name) || "Here", temp: r(c.temperature_2m), feels: r(c.apparent_temperature),
humidity: c.relative_humidity_2m + "%", wind: Math.round(c.wind_speed_10m) + " km/h",
symbol: now[0], summary: now[1], tint: now[2], headline: r(c.temperature_2m) + " " + now[1],
high: r(d.temperature_2m_max[0]), low: r(d.temperature_2m_min[0]), days: days
});
}
JS