├── Complete.png ├── Incomplete.png ├── Obsidian Tasks.js └── README.md /Complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angus-thompson/obsidian-scriptable-tasks-widget/f9166fd3aadf9e46f16f06214b84dca4652488e1/Complete.png -------------------------------------------------------------------------------- /Incomplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angus-thompson/obsidian-scriptable-tasks-widget/f9166fd3aadf9e46f16f06214b84dca4652488e1/Incomplete.png -------------------------------------------------------------------------------- /Obsidian Tasks.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: deep-brown; icon-glyph: magic; 4 | String.prototype.trimRight = function(charlist) { 5 | if (charlist === undefined) charlist = "\s"; 6 | return this.replace(new RegExp("[" + charlist + "]+$"), ""); 7 | }; 8 | 9 | function createWidget(tasks, today, displayTasks, textSize, maxLen, widgetHeight) { 10 | 11 | let linkRegex = /(.+)?(?=\[)(?:\[\[(.+)\]\]|\[(.+)\]\((.+)\))(.+)?/; 12 | 13 | //set up basic widget things 14 | let w = new ListWidget(); 15 | w.url = `obsidian://advanced-uri?vault=${vault}&daily=true`; 16 | 17 | //Title 18 | let titleStack = w.addStack(); 19 | titleStack.topAlignContent(); 20 | let title = titleStack.addText("Tasks"); 21 | title.font = Font.boldSystemFont(textSize*1.5); 22 | title.textColor = Color.dynamic(Color.black(), Color.white()); 23 | title.minimumScaleFactor = 1; 24 | title.url = `obsidian://advanced-uri?vault=${vault}&daily=true&heading=` + title.text; 25 | 26 | titleStack.addSpacer(textSize*1.25); 27 | 28 | //Add new task button 29 | let newTasks = titleStack.addStack(); 30 | let newTasksButton = newTasks.addText("+ Add New"); 31 | newTasksButton.font = Font.semiboldRoundedSystemFont(textSize*1.25); 32 | newTasksButton.textColor = Color.dynamic(Color.gray(), Color.lightGray()); 33 | newTasksButton.minimumScaleFactor = 1; 34 | newTasksButton.url = `obsidian://advanced-uri?vault=${vault}&daily=true&heading=New%20Tasks&data=%22-%20%5B%20%5D%20%22&mode=prepend` 35 | 36 | w.addSpacer(2); 37 | 38 | let mainStack = w.addStack(); 39 | mainStack.layoutHorizontally(); 40 | 41 | let widgetMaxLength = textSize*3+2 42 | 43 | //Adds stacks for each of my task groups 44 | for (let task of displayTasks) { 45 | let stack = mainStack.addStack(); 46 | stack.layoutVertically(); 47 | //Add title 48 | let title = stack.addText(task); 49 | title.url = `obsidian://advanced-uri?vault=${vault}&daily=true&heading=` + title.text.replaceAll(" ", "%20"); 50 | title.textColor = Color.purple(); 51 | title.font = Font.boldSystemFont(textSize*1.25); 52 | title.minimumScaleFactor = 1; 53 | stack.addSpacer(3); 54 | let widgetLength = textSize*3+2 55 | let taskListLength = tasks[task].length; 56 | //If no tasks then print a message 57 | if (tasks[task].length == 0) { 58 | let line = stack.addText((task == `Overdue`) ? `Nothing Overdue!` : (task == "Two weeks") ? `Nothing Due in ${task}` : `Nothing Due ${task}!`); 59 | line.font = Font.boldSystemFont(textSize); 60 | line.textColor = Color.dynamic(Color.black(), Color.white()); 61 | } else { 62 | //Else add the items from the clippeditems, only including up to 18 and adding See More... if more 63 | 64 | //Add each task, including links if necassary and add link to bullet point 65 | tasks[task] = tasks[task].slice(0, maxLen).map((itemArray) => { 66 | item = itemArray[0] 67 | let addTask = stack.addStack(); 68 | addTask.lineLimit = 1; 69 | 70 | let imageManager = FileManager.iCloud(); 71 | let imagePath = imageManager.bookmarkedPath("Scriptable") 72 | let imageOne = imageManager.downloadFileFromiCloud(imagePath + `/Complete.png`); 73 | let imageTwo = imageManager.downloadFileFromiCloud(imagePath + `/Incomplete.png`); 74 | 75 | if (itemArray[4] == today) { 76 | let bullet = addTask.addImage(imageManager.readImage(imagePath + `/Complete.png`)); 77 | bullet.imageSize = new Size(textSize*1.25,textSize*1.25); 78 | bullet.url = URLScheme.forRunningScript() + `?openEditor=false&uriLaunch=true&task=${encodeURIComponent(itemArray[0])}&dateDue=${encodeURIComponent(itemArray[1])}&filePath=${encodeURIComponent(itemArray[2])}&lineNumber=${itemArray[3]}&complete=false` 79 | } else { 80 | let bullet = addTask.addImage(imageManager.readImage(imagePath + `/Incomplete.png`)); 81 | bullet.imageSize = new Size(textSize*1.25,textSize*1.25); 82 | bullet.url = URLScheme.forRunningScript() + `?openEditor=false&uriLaunch=true&task=${encodeURIComponent(itemArray[0])}&dateDue=${encodeURIComponent(itemArray[1])}&filePath=${encodeURIComponent(itemArray[2])}&lineNumber=${itemArray[3]}&complete=true`; 83 | } 84 | //When a task has a link (website or note link), make sure link is interactable, and rest of task is not) 85 | if (linkRegex.test(item)) { 86 | let before = addTask.addText(item.replace(linkRegex, "$1")) 87 | before.font = Font.systemFont(textSize); 88 | before.textColor = Color.dynamic(Color.black(), Color.white()); 89 | before.lineLimit = 1; 90 | 91 | if (item.replace(linkRegex, "$2")) { 92 | let link = addTask.addText(item.replace(linkRegex, "$2")) 93 | //filePath = item.replace(linkRegex, "$2").replaceAll(" ", "%2520") + ".md" 94 | link.url = `obsidian://advanced-uri?vault=${vault}&filepath=` + item.replace(linkRegex, "$2").replaceAll(" ", "%2520") + ".md" 95 | link.textColor = new Color("#7e1dfb"); 96 | link.font = Font.semiboldRoundedSystemFont(textSize); 97 | link.lineLimit = 1; 98 | } else { 99 | let link = addTask.addText(item.replace(linkRegex, "$3")) 100 | link.url = item.replace(linkRegex, "$4") 101 | link.textColor = new Color("#7e1dfb"); 102 | link.font = Font.semiboldRoundedSystemFont(textSize); 103 | link.lineLimit = 1; 104 | } 105 | 106 | let after = addTask.addText(item.replace(linkRegex, "$5")) 107 | after.font = Font.systemFont(textSize); 108 | after.textColor = Color.dynamic(Color.black(), Color.white()); 109 | after.lineLimit = 1; 110 | } else { 111 | let line = addTask.addText(item) 112 | line.font = Font.systemFont(textSize); 113 | line.textColor = Color.dynamic(Color.black(), Color.white()); 114 | line.lineLimit = 1; 115 | } 116 | 117 | //For measuring length of widget to add a spacer later to make sure widget stays aligned to top 118 | widgetLength += textSize*1.25 119 | if (widgetLength > widgetMaxLength) { 120 | widgetMaxLength = widgetLength 121 | } 122 | 123 | return item; 124 | }); 125 | //Add a see more... button if there are more tasks available 126 | if (tasks[task].length < taskListLength) { 127 | let more = stack.addText("See more...") 128 | more.font = Font.semiboldRoundedSystemFont(textSize); 129 | more.textColor = new Color("#8f6fff"); 130 | more.url = `obsidian://advanced-uri?vault=${vault}&daily=true&heading=` + title.text.replaceAll(" ", "%20"); 131 | widgetMaxLength += textSize*1.25 132 | } 133 | stack.minimumScaleFactor = 1; 134 | } 135 | 136 | 137 | mainStack.addSpacer(10); 138 | 139 | } 140 | 141 | w.addSpacer(widgetHeight-widgetMaxLength); 142 | 143 | return w 144 | } 145 | 146 | 147 | // 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" 148 | async function findTasks(today) { 149 | 150 | //Set up file manager, finds the amount of Year folders in the Daily Notes folder and sets up storage arrays 151 | let fileManager = FileManager.iCloud(); 152 | let years = await fileManager.listContents(fileManager.bookmarkedPath(root)); 153 | let overdueTasks = []; 154 | let todayTasks = []; 155 | let tomorrowTasks = []; 156 | let nextTwoWeeksTasks = []; 157 | let longTermTasks = []; 158 | //const dateRegex = /\d\d\d\d-\d\d-\d\d/; 159 | 160 | //Loops through each year folder 161 | for (let year of years) { 162 | if (fileManager.isDirectory(fileManager.bookmarkedPath(root)+ "/" + year)) { 163 | 164 | //Finds each month folder in the year and loops through 165 | let months = await fileManager.listContents(fileManager.bookmarkedPath(root)+ "/" + year); 166 | for (let month of months) { 167 | 168 | //If month is a FOLDER - search for individual notes 169 | if (fileManager.isDirectory(fileManager.bookmarkedPath(root)+ "/" + year + "/" + month)) { 170 | let files = await fileManager.listContents(fileManager.bookmarkedPath(root)+ "/" + year + "/" + month); 171 | for (let file of files) { 172 | 173 | //Download file from icloud, read contents, split by line, store file path 174 | let downloadFile = await fileManager.downloadFileFromiCloud(fileManager.bookmarkedPath(root)+ "/" + year + "/" + month + "/" + file); 175 | let fileContents = await fileManager.readString(fileManager.bookmarkedPath(root)+ "/" + year + "/" + month + "/" + file); 176 | let lines = fileContents.split("\n"); 177 | let originalTaskPath = "/" + year + "/" + month + "/" + file 178 | let lineIndex = 0 179 | 180 | for (let line of lines) { 181 | //Regexp to filter tasks and make a match that has [Full task, Task name, Due date, Task Name if no due date] 182 | let taskRegex = /- \[[x ]\]\s*(.*?)(?:(?=(?:\d{4}-\d{2}-\d{2}))(\d{4}-\d{2}-\d{2})|\s*\n)(?:\s+✅\s*(\d{4}-\d{2}-\d{2}))?/; 183 | let match = line.match(taskRegex); 184 | 185 | //If found a task 186 | if (match) { 187 | let completionDate = parseInt(Date.parse(match[3])); 188 | //If Task has a due date 189 | if (match[3] == null || completionDate == dateToday) { 190 | if (match[2]) { 191 | 192 | //Sets up data to add to task arrays. 193 | let matchName = match[1].trimRight("📅 "); 194 | let date = parseInt(Date.parse(match[2])); 195 | 196 | 197 | //sort task to array based on due date 198 | if (date == dateToday) { 199 | todayTasks.push([matchName, date, match[2], originalTaskPath, lineIndex, match[3]]); 200 | 201 | } else if (date < dateToday) { 202 | overdueTasks.push([matchName, date, match[2], originalTaskPath, lineIndex, match[3]]); 203 | 204 | } else if (date <= tomorrow) { 205 | tomorrowTasks.push([matchName, date, match[2], originalTaskPath, lineIndex, match[3]]); 206 | 207 | } else if (date <= twoWeeks) { 208 | nextTwoWeeksTasks.push([matchName, date, match[2], originalTaskPath, lineIndex, match[3]]); 209 | 210 | } else { 211 | longTermTasks.push([matchName, match[2], originalTaskPath, lineIndex, match[3]]) 212 | } 213 | } 214 | } 215 | 216 | } 217 | lineIndex += 1 218 | } 219 | } 220 | } 221 | } 222 | } 223 | } 224 | 225 | let tasks = {"Overdue" : overdueTasks, "Today" : todayTasks, "Tomorrow" : tomorrowTasks, "Two weeks" : nextTwoWeeksTasks, "Long Term" : longTermTasks}; 226 | 227 | //sort each task list by due date (earliest due at top) 228 | for (task in tasks) { 229 | tasks[task].sort(function(a, b) { 230 | return a[1] - b[1]; 231 | }); 232 | for (x = 0; x < tasks[task].length; x++) { 233 | tasks[task][x] = [tasks[task][x][0], tasks[task][x][2], tasks[task][x][3], tasks[task][x][4], tasks[task][x][5]]; 234 | } 235 | } 236 | 237 | return tasks; 238 | } 239 | 240 | 241 | const uriArguments = args.queryParameters; 242 | const widgetArguments = args.widgetParameter; 243 | 244 | 245 | //Change these to your bookmarked path folder and vault name 246 | const vault = "CHANGE THIS" 247 | const root = "CHANGE THIS"; 248 | let displayTasks = ["Overdue", "Today", "Tomorrow", "Two weeks", "Long Term"]; 249 | if (widgetArguments) { 250 | displayTasks = widgetArguments.split("|"); 251 | } 252 | 253 | 254 | //get today in format YYYY-MM-DD, also gets dateToday in millisecond format and tomorrow, and two weeks so that they can get compared later to decide when tasks are due 255 | const todayDate = new Date(); 256 | const today = `${todayDate.getFullYear()}-${String(todayDate.getMonth() + 1).padStart(2, '0')}-${String(todayDate.getDate()).padStart(2, '0')}` 257 | const dateToday = parseInt(Date.parse(today)); 258 | const tomorrow = dateToday+86400000; 259 | const twoWeeks = dateToday+86400000*14; 260 | 261 | let tasks = await findTasks(today); 262 | //const textSize = 12 263 | //const maxLen = 17 264 | 265 | if (config.runsInWidget) { 266 | let textSize = 0; 267 | let maxLen = 0 268 | //widget height is very dependent on device type 269 | let widgetHeight = 0 270 | if (config.widgetFamily == "extraLarge") { 271 | textSize = 12; 272 | maxLen = 17; 273 | widgetHeight = 288.5 274 | } else if (config.widgetFamily == "large") { 275 | textSize = 17; 276 | maxLen = 12; 277 | widgetHeight = 288.5 278 | } else if (config.widgetFamily == "medium") { 279 | textSize = 17; 280 | maxLen = 3; 281 | widgetHeight = 130 282 | } else if (config.widgetFamily == "small") { 283 | textSize = 10; 284 | maxLen = 12; 285 | widgetHeight = 130 286 | } else if (config.widgetFamily == null) { 287 | textSize = 12; 288 | maxLen = 17; 289 | widgetHeight = 288.5 290 | } 291 | 292 | let widget = createWidget(tasks, today, displayTasks, textSize, maxLen, widgetHeight); 293 | Script.setWidget(widget); 294 | Script.complete(); 295 | } else if (uriArguments.uriLaunch) { 296 | App.close(); 297 | let fileManager = FileManager.iCloud(); 298 | let downloadFile = await fileManager.downloadFileFromiCloud(fileManager.bookmarkedPath(root) + uriArguments.filePath); 299 | let fileLines = fileManager.readString(fileManager.bookmarkedPath(root) + uriArguments.filePath); 300 | fileLines = fileLines.split("\n"); 301 | let replaceRegex = new RegExp(`- \\[[ x]\\] ${uriArguments.task.replaceAll("\[", "\\[").replaceAll("\]", "\\]").replaceAll("\(", "\\(").replaceAll("\)", "\\)")}.*📅.*${uriArguments.dateDue}.*`) 302 | 303 | for (let i = 0; i < fileLines.length; i++) { 304 | if (fileLines[i].match(replaceRegex)) { 305 | if (uriArguments.complete == "true") { 306 | fileLines[i] = "- [x] " + uriArguments.task + " 📅 " + uriArguments.dateDue + " ✅ " + today; 307 | } else { 308 | fileLines[i] = "- [ ] " + uriArguments.task + " 📅 " + uriArguments.dateDue; 309 | } 310 | } 311 | } 312 | 313 | fileLines = fileLines.join("\n"); 314 | fileManager.writeString(fileManager.bookmarkedPath(root) + uriArguments.filePath, fileLines); 315 | } else { 316 | const textSize = 12; 317 | const maxLen = 17; 318 | let widget = createWidget(tasks, today, displayTasks, textSize, maxLen); 319 | widget.presentExtraLarge(); 320 | } 321 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # obsidian-scriptable-tasks-widget 5 | ![A5927A3A-60DE-4A3A-8324-114FDF7CA5BB](https://user-images.githubusercontent.com/98095245/225629458-ec7bdbeb-2734-460a-ba18-7e3316a5894f.jpeg) 6 | 7 | This is javascript code to display my tasks from obsidian in an iPad OS extra large widget using [Scriptable](https://scriptable.app) 8 | 9 | There are a few steps to set up. First you need to designate a bookmarked path in the scriptable app. Then you can change the required variables in the script and it should basically work 10 | 11 | That is assuming your daily notes are sorter into DailyNotes/Years/Months/dailynote.md format. If they aren't you might need to do some editing of the file finder function. 12 | 13 | It is annoying having to launch the scriptable app each time a link is clicked, however I think this is just a limitation of how ios handles uri links. 14 | 15 | This is my first Javascript project so I am sure there are lots of technical issue with it, however it runs and it works for what I need 16 | 17 | Also, apparently there may be some issues if there are too many daily notes, however I have not run into this issue 18 | 19 | Feel free to edit this, improve upon it, I'm not quite sure how github works but if you can submit improvements to here that would be appreciated. 20 | 21 | Again it's not the prettiest and best solution probably, but its the best ive seen and can use. 22 | 23 | (Also this assumes your scriptable files are accessible with the bookmarked path "Scriptable" -- this is for the checkmark images) 24 | 25 | ### **INSTALLATION STEPS** 26 | 27 | Add the images to your scriptable folder 28 | Change the variables Vault and Root to the required paths for accessing your obsidian files 29 | 30 | Add a widget to your homescreen, in the widget paramaters type in the task lists that you would like to show (smaller widgets than extraLarge are really only good at showing one list) Format for this is -- Today|Tomorrow|Two Weeks etc. 31 | 32 | Should be all good 33 | 34 | Do note that spacing and things are hard coded and probable need to be edited depending on your phone or device. The values to edit this would be the widgetheight variable. Also textsize and max displayed lines are easily changable 35 | 36 | 37 | 38 | --------------------------------------------------------------------------------