All widgets
Currency
Comes with Chopstick
Type an amount of money in search, like 100 usd in eur or €20 to yen, to see it converted at the European Central Bank's latest rate. Return copies the answer; click it to edit.
Already installed? It's in your widget library.


Type it in the notch's search
- Convert amount
What it runs
A widget is a folder of plain files. Here is all of its code.
widget.json
{
"id": "currency",
"name": "Currency",
"icon": "icon.png",
"description": "Type an amount of money in search, like 100 usd in eur or €20 to yen, to see it converted at the European Central Bank's latest rate. Return copies the answer; click it to edit.",
"anytime": false,
"initial": "help",
"commands": {
"convert": {
"description": "Convert an amount of money to another currency and copy the answer (100 usd in eur, $20, €5 to yen).",
"phrase": "Convert {amount}",
"inputs": {
"amount": { "type": "money", "description": "An amount with a currency code, symbol or name, optionally followed by \"in\" or \"to\" and the currency to convert to" }
},
"preview": { "state": "converted", "run": "rate.sh" },
"do": [ { "run": "rate.sh", "else": "failed" }, { "copy": "{{result.text}}" }, { "go": "copied" } ]
},
"show": {
"description": "Show an amount of money converted to another currency.",
"inputs": {
"amount": { "type": "money", "description": "An amount with a currency code, symbol or name, optionally followed by \"in\" or \"to\" and the currency to convert to" }
},
"do": [ { "set": { "error": "" } }, { "run": "rate.sh", "then": "converted", "else": "failed" } ]
}
},
"states": {
"help": {
"task": true,
"open": true,
"compact": { "leading": { "symbol": "dollarsign.arrow.circlepath" } },
"expanded": {
"size": "regular",
"header": { "symbol": "dollarsign.arrow.circlepath", "title": "Currency" },
"blocks": [
{ "input": { "placeholder": "100 usd in eur", "do": [ { "command": "show", "with": { "amount": "{{item.text}}" } } ] } },
{ "note": "{{error | Or type an amount in search (⌘Space): 100 usd in eur, €20 to yen, $5.}}" }
]
}
},
"converted": {
"task": true,
"open": true,
"compact": { "leading": { "symbol": "dollarsign.arrow.circlepath" }, "trailing": { "text": "{{result.text}}" } },
"expanded": {
"size": "regular",
"header": { "symbol": "dollarsign.arrow.circlepath", "title": "Currency" },
"blocks": [
{ "row": [
{ "stack": [ { "text": "{{input.amount.text}}", "style": "title" }, { "text": "{{input.amount.name}}", "style": "caption" } ] },
{ "text": "→", "style": "title" },
{ "stack": [ { "text": "{{result.text | …}}", "style": "title" }, { "text": "{{input.amount.to.name}}", "style": "caption" } ] }
], "align": "center", "spacing": "loose" },
{ "note": "{{result.rate | Getting the rate…}}" },
{ "note": "{{result.date}}" },
{ "note": "{{error}}" },
{ "input": { "placeholder": "100 usd in eur", "value": "{{input.amount.text}} in {{input.amount.to.code}}",
"do": [ { "command": "show", "with": { "amount": "{{item.text}}" } } ] } }
]
},
"keys": [ { "key": "cmd+c", "label": "Copy", "do": [ { "copy": "{{result.text}}" }, { "go": "copied" } ] } ]
},
"failed": {
"task": true,
"open": true,
"compact": { "leading": { "symbol": "exclamationmark.triangle", "tint": "orange" } },
"expanded": {
"size": "regular",
"header": { "symbol": "dollarsign.arrow.circlepath", "title": "Currency" },
"blocks": [
{ "note": "{{error | Couldn't get the exchange rate.}}" },
{ "input": { "placeholder": "100 usd in eur", "value": "{{input.amount.text}} in {{input.amount.to.code}}",
"do": [ { "command": "show", "with": { "amount": "{{item.text}}" } } ] } }
]
}
},
"copied": {
"task": true,
"dismiss_after": 2,
"compact": { "leading": { "symbol": "doc.on.doc", "tint": "green" }, "trailing": { "text": "{{result.text}}" } }
}
}
}
rate.sh
#!/bin/sh
# Converts the typed amount (input.amount in $CHOPSTICK_DATA) at the European Central Bank's
# latest rates, from Frankfurter (no key). All rates come in one request, kept for an hour.
dir="${CHOPSTICK_STORAGE:-${CHOPSTICK_TMP:-/tmp}}"
rates="$dir/rates.json"
if [ -z "$(find "$rates" -mmin -60 2>/dev/null)" ]; then
curl -fsS --max-time 8 "https://api.frankfurter.dev/v1/latest?base=EUR" > "$rates.new" && mv "$rates.new" "$rates" \
|| { rm -f "$rates.new"; [ -s "$rates" ] || { echo "Couldn't reach the exchange rate service" >&2; exit 1; }; }
fi
osascript -l JavaScript - "$CHOPSTICK_DATA" "$(cat "$rates")" <<'JS'
function run(argv) {
const data = JSON.parse(argv[0]), ecb = JSON.parse(argv[1]);
const amount = (data.input || {}).amount;
if (!amount) throw new Error("No amount to convert");
const rate = code => code === "EUR" ? 1 : ecb.rates[code];
const from = amount.code, to = amount.to.code;
if (!rate(from) || !rate(to)) throw new Error("No rate for " + (rate(from) ? to : from));
const per = rate(to) / rate(from), value = amount.value * per;
// No cents for currencies that don't use them.
const whole = ["JPY", "KRW", "HUF", "IDR", "ISK"];
const show = (n, code) => n.toLocaleString("en-US", { maximumFractionDigits: whole.includes(code) ? 0 : 2,
minimumFractionDigits: whole.includes(code) ? 0 : 2 }) + " " + code;
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const [y, m, d] = ecb.date.split("-").map(Number);
return JSON.stringify({
value: value, text: show(value, to), from: show(amount.value, from),
rate: "1 " + from + " = " + per.toPrecision(5) + " " + to,
date: "ECB rate of " + d + " " + months[m - 1] + " " + y
});
}
JS