My Scriptable iOS Widget for Tasks (with Tasks Plugin)

First off this idea was originally posted by Lime.

However, I have not used any of Limes code partly as it wasn’t available, but I also believe I have added substantially upon their idea, so this is all my own work and therefore I think it deserves it’s own topic.

Widget output and function

This widget is designed for iPad OS due to the extra large sized widget. It basically displays all my tasks to do in 5 lists: “Overdue”, “Today”, “Tomorrow”, “Two weeks”, “Long Term”. Each title is clickable to open the daily note at that Heading. Further, tasks that have [[noteTitles]] or [weblinks](google.com) have clickable titles that will take you to that note or website. The whole widget is clickable to open to the Daily Note. The title “Tasks” is clickable to open the Tasks section of my daily note.

It can be modified to only show two lists pretty easily and that would fit into a normal Large Widget.

It is pretty personalised to my Vault due to the way that I store my daily notes (Daily Notes/YYYY/MM-MMMM/note.md), although I’m sure it could be modified for use.

This is also my first time coding JavaScript, so I am sure it could be more efficient, however I tried to use ChatGPT to help as much as I could.

Here is an example widget (I have just added duplicate tasks to flesh out the example.

Here is the code



String.prototype.trimRight = function(charlist) {
  if (charlist === undefined) charlist = "\s";
  return this.replace(new RegExp("[" + charlist + "]+$"), "");
};

function createWidget(items, today) {
  
  let clippedItems = items.slice(0, 5);
  let linkRegex = /(.+)?(?=\[)(?:\[\[(.+)\]\]|\[(.+)\]\((.+)\))(.+)?/;
	
  //set up basic widget things
  let w = new ListWidget();
  w.url = "obsidian://advanced-uri?vault=VAULT&daily=true"
  let titleStack = w.addStack();
  let title = titleStack.addText("Tasks");
  title.font = Font.boldSystemFont(18);
  title.textColor = Color.dynamic(Color.black(), Color.white());
  title.minimumScaleFactor = 1;
  title.url = "obsidian://advanced-uri?vault=VAULT&daily=true&heading=" + title.text;
  w.addSpacer(2);

  let mainStack = w.addStack();
  mainStack.layoutHorizontally();
	
  //Adds stacks for each of my task groups
  for (let i = 0; i < 5; i++) {
    let stack = mainStack.addStack();
    stack.layoutVertically();
		//Add title
    let title = stack.addText(["Overdue", "Today", "Tomorrow", "Two Weeks", "Long Term"][i]);
    title.url = "obsidian://advanced-uri?vault=VAULT&daily=true&heading=" + title.text.replaceAll(" ", "%2520");
    title.textColor = Color.purple();
    title.font = Font.boldSystemFont(15);
    title.minimumScaleFactor = 1;
    stack.addSpacer(3);
    
    //If no tasks then print a message
    if (clippedItems[i].length === 0) {
      let line = stack.addText(`Nothing ${["Overdue", "Due Today", "Due Tomorrow", "Due in Two Weeks", "Due Long Term"][i]}!`);
      line.font = Font.boldSystemFont(12);
      line.textColor = Color.dynamic(Color.black(), Color.white());
    } else {
      //Else add the items from the clippeditems, only including up to 17 and adding See More... if more
      let maxLen = 16;
      
      clippedItems[i] = clippedItems[i].slice(0, maxLen).map((item)  => {
      	let task = stack.addStack();
        task.lineLimit = 1;
        let bullet = task.addText("▫️");
	      bullet.font = Font.systemFont(12);
 	   	  bullet.textColor = Color.dynamic(Color.black(), Color.white());
        
      	if (linkRegex.test(item)) {
  	      let before = task.addText(item.replace(linkRegex, "$1"))
   	  	  before.font = Font.systemFont(12);
    	    before.textColor = Color.dynamic(Color.black(), Color.white());
          before.lineLimit = 1;
        	if (item.replace(linkRegex, "$2")) {
	          let link = task.addText(item.replace(linkRegex, "$2"))
            link.textColor = new Color("#7e1dfb");
   		   		link.font = Font.semiboldRoundedSystemFont(12);
            filePath = item.replace(linkRegex, "$2").replaceAll(" ", "%2520") + ".md"
            link.url = "obsidian://advanced-uri?vault=VAULT&filepath=" + filePath
            link.lineLimit = 1;
          } else {
            let link = task.addText(item.replace(linkRegex, "$3"))
            link.textColor = new Color("#7e1dfb");
   		   		link.font = Font.semiboldRoundedSystemFont(12);
            link.url = item.replace(linkRegex, "$4")
            link.lineLimit = 1;
          }
          
        	let after = task.addText(item.replace(linkRegex, "$5"))
        	after.font = Font.systemFont(12);
        	after.textColor = Color.dynamic(Color.black(), Color.white());
          after.lineLimit = 1;
        
        } else {
          let line = task.addText(item)
          line.font = Font.systemFont(12);
          line.textColor = Color.dynamic(Color.black(), Color.white());
          line.lineLimit = 1;
        }
      	return item;
      });
      
      if (clippedItems[i].length == 16) {
        let more = stack.addText("See more...")
        more.font = Font.semiboldRoundedSystemFont(12);
        more.textColor = new Color("#8f6fff");
        more.url = "obsidian://advanced-uri?vault=VAULT&daily=true&heading=" + title.text.replaceAll(" ", "%2520");
      }
      stack.minimumScaleFactor = 1;
    }
    
    mainStack.addSpacer(14);
    
  }
  
  return w
}


// This is the main function to comb through my folder structure for every daily note- Comb each note for something that matches a regex "- [ ] xxx" with out without an ending date "YYYY-MM-DD"
async function findTasks(todayTitle) {
  
  //Set up file manager, finds the amount of Year folders in the Daily Notes folder and sets up storage arrays
  let fileManager = FileManager.iCloud();
  let years = await fileManager.listContents(fileManager.bookmarkedPath(root));
  let overdueTasks = [];
  let todayTasks = [];
  let tomorrowTasks = [];
  let nextTwoWeeksTasks = [];
  let longTermTasks = [];
  
  //Loops through each year folder
  for (let year of years) { 
    if (fileManager.isDirectory(fileManager.bookmarkedPath(root)+ "/" + year)) {
      
      //Finds each month folder in the year and loops through
      let months = await fileManager.listContents(fileManager.bookmarkedPath(root)+ "/" + year);
      for (let month of months) {
        
        //If month is a folder of years - search
        if (fileManager.isDirectory(fileManager.bookmarkedPath(root)+ "/" + year + "/" + month)) {
          let files = await fileManager.listContents(fileManager.bookmarkedPath(root)+ "/" + year + "/" + month);
          for (let file of files) {
            
            let downloadFile = await fileManager.downloadFileFromiCloud(fileManager.bookmarkedPath(root)+ "/" + year + "/" + month + "/" + file);
            let fileContents = await fileManager.readString(fileManager.bookmarkedPath(root)+ "/" + year + "/" + month + "/" + file);
            let lines = fileContents.split("\n");

            for (let line of lines) {
              //Regexp to filter tasks and make a match that has [Full task, Task name, Due date, Task Name if no due date]
              let taskRegex = new RegExp("- \\[ \\] (.*) (\\d\\d\\d\\d-\\d\\d-\\d\\d)|- \\[ \\] (.+)");
              let match = line.match(taskRegex);
              
              
              //If found a task
              if (match) {
                //If Task has a due date
                if (match[2]) {
                  
                  let matchName = match[1].trimRight("📅");
                  let date = Date.parse(match[2]);
                  date = parseInt(date);
                  let dateToday = Date.parse(todayTitle);
                  dateToday = parseInt(dateToday);
                  let tomorrow = dateToday+86400000;
                  let twoWeeks = dateToday+86400000*14;
									let dateRegex = new RegExp("(\\d\\d\\d\\d-\\d\\d-)(\\d\\d)")
                  let dueDay = match[2].replace(dateRegex, "$2");
                  
                  //sort task to array based on due date
                  if (date == dateToday) {
                    todayTasks.push([matchName, date]);
                    
                  } else if (date < dateToday) {
                    overdueTasks.push([matchName, date]);
                    
                  } else if (date <= tomorrow) {
                    tomorrowTasks.push([matchName, date]);
                    
                  } else if (date <= twoWeeks) {
                    //Add date to these - I would like to sort them too but I cant be bothered
                    nextTwoWeeksTasks.push([matchName, date]);
                    
                  }
                  
                } else {
                  let matchName = match[3].trimRight("📅");
                  longTermTasks.push(matchName)
                }

              }
            }
          }
        }
      }
    }
  }
  
	let tasks = [overdueTasks, todayTasks, tomorrowTasks, nextTwoWeeksTasks, longTermTasks];

  for (let i = 0; i < 4; i++) {
    tasks[i].sort(function(a, b) {
  		return a[1] - b[1];
		});
  	for (let x = 0; x < tasks[i].length; x++) {
      tasks[i][x] = tasks[i][x][0];
    }
  }

  return tasks;
}

//My Root Folder
const root = "Daily Notes";
//get today in format YYYY-MM-DD
let today = new Date();
let todayTitle = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`

let items = await findTasks(todayTitle);
let widget = createWidget(items, todayTitle);

if (config.runsInWidget) {
  Script.setWidget(widget);
  Script.complete();
} else {
  widget.presentExtraLarge();
}

It works pretty well, there are a few issues/features I’d like to add:

  • tasks with links are displayed as two/three separate stacks causing display issue (see task 2 in “Long Term”)
  • Other issues with spacing that I can’t control (seems to be dependent on length of task names)
  • A lot of processing is hard coded. I’m not sure it will work yet with links that have aliases or heading links (e.g. [[title|heading]]) This probably will result in more issues but I haven’t found any yet. Also probably an easy fix.
  • Due to limitations in ios/Scriptable it has to open scriptable first before opening obsidian
  • I would like to add clickable task bullets to mark the task as done, however I think that interaction would have to open the Scriptable first then call the url (from Obsidan advanced uri) to update the note. I may come back and add this anyway.
  • Lime said there may be issues with large note libraries when doing this, I am not sure, it probably depends on file sizes
  • Probably more…
2 Likes

I know this hasn’t got traction here but people on reddit wanted to see the code so i updated it a bit to include completing tasks by clicking the bullet points, and i have posted the code on github. There is a video of the widget in action on the github too.

1 Like