iOS Auto updating kanji progress wallpaper

Hello! I recently created an auto updating wallpaper to show my current progress on the wanikani kanji, using the iOS app Scriptable and Siri Shortcuts

Right now the numbers are hard coded to iPhone XS Max resolution, but they should be easy to adjust.

I have it set to run the script every time I close my wanikani app. It keeps a cache and doesn’t request the whole payload every time, but if you need to force update it you can remove the “//“ from “ // override = true” and run the shortcut (remember to set it back once you’re done)

To add the shortcut:
1: click the link (it should open in shortcuts)
2: scroll to the bottom and click “add untrusted shortcut”
3: it should ask you for your wanikani api token, get it from the website and paste it in (Do NOT click the green plus on the left. Just paste the token in)
4: click done

Once the shortcut is added, you can set up the auto reloading like so:

1: click the automation tab in the shortcuts app
2: click the plus in the upper right hand corner
3: click “personal automation” (this may not appear if you do not have a home set up through apple. If it doesn’t ask then it should default to personal and you can continue)
4: select “App” from the available options
5: choose your app and select “is closed”
6: click add action
7: search for “run shortcut” and select the new shortcut
8: click next, then done

Hope this newer version is simpler to set up, and the instructions are fully clear



Scriptable script:

const headers = {"Authorization": "Bearer <WANIKANI_API_TOKEN>"};

const cache_filename = "wk_wallpaper_data.json";

const dimensions = {
  width: 1418,
  height: 3072,
  top_buffer: 192 + 132,
  bottom_buffer: 192 + 102,
  left_buffer: 88,
  right_buffer: 88,

const colors = [

async function get_wk_data() {
  let fm = FileManager.local()
  let path = fm.joinPath(fm.cacheDirectory(), cache_filename)

let override = false
// override = true
  if(!fm.fileExists(path) || override) {
    let url = "";

    kanji = {}
    while(url) {
      let request = new Request(url);
      request.headers = headers;
      let response = await request.loadJSON();
      url = response.pages.next_url;
  => {
        kanji[] =
    url = ''
    srs = {}
    while(url) {
      let request = new Request(url);
      request.headers = headers;
      let response = await request.loadJSON();
      url = response.pages.next_url;
    => {
        srs[] =;
    let wk_data = {
    let image_params = await calculate_image_params(wk_data)
    let cache_data = {
      updated_time: new Date().toISOString(),
    await fm.writeString(path, JSON.stringify(cache_data))
    return cache_data
  } else {
    let cache_data = await JSON.parse(fm.readString(path))
    url = '' + cache_data.updated_time

    let updated = false
    while(url) {
      let request = new Request(url);
      request.headers = headers;
      let response = await request.loadJSON();
      url = response.pages.next_url;
    => {
        let id =
        let old_srs = cache_data.wk_data.srs[id]
        let new_srs =
        if(colors[old_srs] != colors[new_srs]) {
          updated = true
        cache_data.wk_data.srs[id] = new_srs;
    cache_data.updated_time = new Date().toISOString()
    await fm.writeString(path, JSON.stringify(cache_data))
    if(!updated) {
    return cache_data

async function calculate_image_params(wk) {
  const w = dimensions.width - dimensions.left_buffer - dimensions.right_buffer
  const h = dimensions.height - dimensions.top_buffer - dimensions.bottom_buffer

  potential_s = []
  let r = 1
  let n = Object.keys(wk.kanji).length
//   n = 14269
  for(let i = 1; i <= w; i++) {
    potential_s.push(w / i)
  for(let j = 1; j <= h; j++) {
    potential_s.push(h / j / r)
  potential_s.sort((a, b) => b - a)
  let s
  let rows
  let cols
  let x_offset
  let y_offset
  for(let i = 0; i < potential_s.length; i++) {
    s = potential_s[i]
    cols = Math.floor(w / s)
    rows = Math.floor(h / r / s)
    if(rows * cols < n) {
    x_offset = (w - cols * s) / 2
    y_offset = (h - rows * r * s) / 2
  return {
    adjusted_width: w,
    adjusted_height: h,

async function get_image_data(
) {
  let cx = new DrawContext()
  cx.size = new Size(dimensions.width, dimensions.height)
  cx.opaque = true
  cx.fill(new Rect(0, 0, dimensions.width, dimensions.height))
  let cols = data.image_params.cols
  let s = data.image_params.s
  let r = data.image_params.r
  let l_buff = dimensions.left_buffer + data.image_params.x_offset
  let t_buff = dimensions.top_buffer + data.image_params.y_offset
  let kanji = Object.keys(data.wk_data.kanji)
  for(let i = 0; i < kanji.length; i++) {
    let x = i % data.image_params.cols
    let y = Math.floor(i / data.image_params.cols)
    let k = kanji[i]
    let id = data.wk_data.kanji[k]
    let srs = data.wk_data.srs[id] || 0
    let color = new Color(colors[srs])
    let rect = new Rect(
      l_buff + x * s,
      t_buff + y * s * r,
      s * r,
    cx.drawTextInRect(kanji[i], rect)
  let image = cx.getImage()
  return image

try {
  let data = await get_wk_data()
  if(data !== undefined) {
    let image = await get_image_data(
    let raw_image = Data.fromPNG(image)
    let base64_image = raw_image.toBase64String()
} finally {



Looks great! Can you clarify what you mean by “wanikani app” as far as I know there’s no official app – do you mean Tsurukame, or do you mean just running the mobile on Safari or another browser app?

It looks great!

Can I ask, I got this error:
What should I do?

I got this same error, which was resolved when I put in the API token correctly. Make sure to get rid of the ‘<’ and ‘>’ when you put yours in on the top of the page of code. Hope you can get it to work!


You can set it to fire after any app. I use tsurukame most the time and juken if I’m in a hurry, so it runs after both

Sorry for the late response, looks like RedFlameUDN answered your question? It looks to me like a missing api token.

Note there should be a space after “Bearer” in the string

1 Like

Awesome! It works great for me. Thanks a lot for putting this together! :pray:t2:

Here’s a page that includes all the iPhone resolutions for anyone else that needs it:


I look forward to my Lock Screen looking like yours! One extra thing, you need to make sure the top buffer and bottom buffer are also correct for your phone model.

I used the “full blueprint” version here and measured the pixels in photoshop.

1 Like

I have an iPhone 11 and the wallpaper appears to work correctly as-is.

Thanks for the heads up. I tried changing the buffer sizes before I posted (without any guidance) and I kept getting an error saying something like “scripts are required to have an output” or something like that, so I just switched it back to what you already had. I will check out that site and give it a shot for my model (iPhone X).

Sorry I never used scriptable or siri shortcut. I imported the script and added it to shortcut. It’s runs without any errors (I change the API etc and I can run the script from Shortcuts everything is ok on this side).
But I don’t get how / where you get back the image and how you set it as a wallpaper (there is nothing inside the “photos” app).

I looked around online to see how this app worked but what you’re trying to do here is very specific or I don’t have the good keywords.
If you can do a quick guide for this.

Thank you so much :slight_smile:

You need to use the shortcut I linked. The shortcut decodes the output as base64 and runs the set wallpaper shortcut

One thing I’ve found is that if you restart your phone the background gets messed up. You can either wait for one of your kanji to change color or force an override to fix it. It will go back on its own if you’re fine having it be incorrect for a short time until one of your apprentice kanji gets bumped to guru or something

Oh yeah ok just needed to accept instrusted script before. Works like a charm !

Thank you so much !

1 Like

Thank you for the wonderful skript. Unfortunately it does not seem to work for me. So what I did was download Skriptable. Then I copy and pasted your skript and changed the API key. I then downloaded your shortcut for siri and set my phone to trust untrusted shortcuts. I then renamed the skript to “Wanikani Wallpaper” so that the shortcut could find it. The skript runs in skriptable without an error message and the shortcut also runs without giving me an error, but when I then check my Lock Screen it hasn’t changed. Also I am on an Iphone XR, do you maybe have any idea how I could fix it? Sorry for taking your time with something that might be a very small thing but I just can’t figure it out.

I fixed it by forcing an update as you said, there was probably an empty wallpaper cached or something idk.

Hi guys !
I’m getting this error … I’m a noob at this so please bare with me >.<image

This is perhaps the coolest use of siri shortcuts I have ever seen.

Looks really good on my phone, Thank you!

1 Like

@maboesanman thanks! It looks beautiful!

It looks like you deleted a semicolon. Make sure you only replace <WANIKANI_API_TOKEN> and leave the quotes, curly braces, and semicolons