Skip to content

Instantly share code, notes, and snippets.

@josh33
Created September 14, 2024 12:51
Show Gist options
  • Select an option

  • Save josh33/1a0ba782312ca2df8d60a6d8a1f69f10 to your computer and use it in GitHub Desktop.

Select an option

Save josh33/1a0ba782312ca2df8d60a6d8a1f69f10 to your computer and use it in GitHub Desktop.
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-purple; icon-glyph: tasks;
const vaultName = "vaultname" // Replace with your actual vault name added as a file bookmark in Obsidian. Have not tested with iCloud syncing vaults.
// const excludeFolders = ["200 Spiritual/_Scriptures", "_Attachments", "_Templates"]
const excludeFolders = ["comma","delimited"] // if you have any large folders that will never have active tasks, add them here.
const lightColors = {
background: "#FFFFFF",
text: "#000000",
accent: "#007AFF",
pill: "#E0E0E0",
pillText: "#000000",
circle: "#C7C7CC"
}
const darkColors = {
background: "#000000",
text: "#FFFFFF",
accent: "#0A84FF",
pill: "#2C2C2E",
pillText: "#FFFFFF",
circle: "#48484A"
}
function getColors() {
return Device.isUsingDarkAppearance() ? darkColors : lightColors
}
// Function to get all markdown files recursively
async function getMarkdownFiles(dir) {
const files = []
const items = await FileManager.local().listContents(dir)
for (const item of items) {
const path = `${dir}/${item}`
const isDirectory = await FileManager.local().isDirectory(path)
if (isDirectory) {
if (!excludeFolders.includes(item)) {
files.push(...await getMarkdownFiles(path))
}
} else if (item.endsWith('.md')) {
files.push(path)
}
}
return files
}
// Function to parse tasks from a file
async function parseTasksFromFile(filePath) {
const content = await FileManager.local().readString(filePath)
const lines = content.split('\n')
const tasks = []
const dateRegex = /(📅|🗓️|🛫|🛬|↓|🔁|📆)\s*(\d{4}-\d{2}-\d{2})/g
const emojiMap = {
'📅': 'Due',
'🗓️': 'Scheduled',
'🛫': 'Start',
'🛬': 'End',
'↓': 'Scheduled',
'🔁': 'Repeat',
'📆': 'Date'
}
for (const line of lines) {
if (line.match(/^- \[ \]/)) {
let taskText = line.replace(/^- \[ \] /, '').trim()
let dates = {}
const matches = [...taskText.matchAll(dateRegex)]
for (const match of matches) {
const [fullMatch, emoji, date] = match
dates[emojiMap[emoji]] = date
taskText = taskText.replace(fullMatch, '').trim()
}
const task = {
text: taskText,
dates: dates,
filePath: filePath
}
tasks.push(task)
}
}
return tasks
}
function getFileName(filePath) {
const parts = filePath.split('/')
return parts[parts.length - 1].replace('.md', '')
}
// Function to prioritize tasks
function prioritizeTasks(tasks) {
const now = new Date()
now.setHours(0, 0, 0, 0)
function getDateValue(task, dateType) {
const date = task.dates[dateType]
return date ? new Date(date) : null
}
function compareDates(dateA, dateB) {
if (!dateA && !dateB) return 0
if (!dateA) return 1
if (!dateB) return -1
return dateA - dateB
}
return tasks.sort((a, b) => {
// Sort by status (overdue, dueToday, scheduled, unplanned)
const statusA = getTaskStatus(a, now)
const statusB = getTaskStatus(b, now)
if (statusA !== statusB) {
return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB)
}
// Sort by due date
const dueDateA = getDateValue(a, 'Due')
const dueDateB = getDateValue(b, 'Due')
const dueComparison = compareDates(dueDateA, dueDateB)
if (dueComparison !== 0) return dueComparison
// Sort by scheduled date
const scheduledDateA = getDateValue(a, 'Scheduled')
const scheduledDateB = getDateValue(b, 'Scheduled')
const scheduledComparison = compareDates(scheduledDateA, scheduledDateB)
if (scheduledComparison !== 0) return scheduledComparison
// Sort by start date
const startDateA = getDateValue(a, 'Start')
const startDateB = getDateValue(b, 'Start')
const startComparison = compareDates(startDateA, startDateB)
if (startComparison !== 0) return startComparison
// If all else is equal, sort by task text
return a.text.localeCompare(b.text)
})
}
const statusOrder = ['overdue', 'dueToday', 'scheduled', 'unplanned']
function getTaskStatus(task, now) {
const dueDate = task.dates['Due'] ? new Date(task.dates['Due'] + 'T00:00:00') : null
const scheduledDate = task.dates['Scheduled'] ? new Date(task.dates['Scheduled'] + 'T00:00:00') : null
if (dueDate) {
if (dueDate < now) return 'overdue'
if (dueDate.getTime() === now.getTime()) return 'dueToday'
}
if (scheduledDate) return 'scheduled'
return 'unplanned'
}
// Function to classify tasks into "assigned to me" or "waiting on others"
function classifyTasks(tasks) {
const assignedToMe = []
const waitingOnOthers = []
for (const task of tasks) {
const isAssignedToOthers = task.text.includes("[[@") || task.filePath.includes("/@")
if (isAssignedToOthers) {
waitingOnOthers.push(task)
} else {
assignedToMe.push(task)
}
}
return { assignedToMe, waitingOnOthers }
}
// Function to extract the first name from a task or file
function extractFirstName(task) {
let match = task.text.match(/\[\[@(\w+)/) || task.filePath.match(/\/@(\w+)/)
return match ? match[1] : "Unknown"
}
// Function to format the due date
function formatDueDate(dueDate) {
const now = new Date()
now.setHours(0, 0, 0, 0)
// Parse the date string and adjust for local timezone
const due = new Date(dueDate + 'T00:00:00')
let color
let status
let formattedDate
const diffTime = Math.abs(due - now)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (due < now) {
color = new Color("#FF3B30") // Red for overdue
status = 'past'
formattedDate = diffDays === 1 ? "1 day ago" : `${diffDays} days ago`
} else if (due.getTime() === now.getTime()) {
color = new Color("#FF9500") // Orange for today
status = 'today'
formattedDate = "today"
} else {
color = new Color("#34C759") // Green for future dates
status = 'future'
formattedDate = diffDays === 1 ? "in 1 day" : `in ${diffDays} days`
}
return { text: formattedDate, color: color, status: status }
}
// Function to parse basic markdown links
function parseMarkdownLinks(text) {
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
return text.replace(linkRegex, '$1')
}
// Function to load images
function loadImage(imageName) {
const fm = FileManager.iCloud()
const dir = fm.documentsDirectory()
const path = fm.joinPath(dir, `icons/${imageName}.png`)
if (fm.fileExists(path)) {
return fm.readImage(path)
} else {
console.log(`${imageName}.png does not exist. Use fileExists(filePath) on a FileManager to check if a file exists.`)
return null
}
}
// Main function to create the widget
async function createWidget() {
const colors = getColors()
const widget = new ListWidget()
widget.backgroundColor = new Color(colors.background)
widget.setPadding(20, 20, 20, 20)
const vault = FileManager.local().bookmarkedPath(vaultName)
const files = await getMarkdownFiles(vault)
let allTasks = []
for (const file of files) {
const tasks = await parseTasksFromFile(file)
allTasks.push(...tasks)
}
const { assignedToMe, waitingOnOthers } = classifyTasks(allTasks)
const prioritizedAssignedToMe = prioritizeTasks(assignedToMe)
const prioritizedWaitingOnOthers = prioritizeTasks(waitingOnOthers)
const totalAssignedToMe = prioritizedAssignedToMe.length
const totalWaitingOnOthers = prioritizedWaitingOnOthers.length
const myTasksStack = widget.addStack()
myTasksStack.layoutHorizontally()
const myTasksTitle = myTasksStack.addText("My Tasks")
myTasksTitle.font = Font.boldSystemFont(16)
myTasksTitle.textColor = new Color(colors.accent)
myTasksStack.addSpacer()
const myTasksCount = myTasksStack.addText(`(${totalAssignedToMe})`)
myTasksCount.font = Font.mediumSystemFont(14)
myTasksCount.textColor = new Color(colors.accent)
widget.addSpacer(4)
function addTaskToWidget(task, widget, isWaitingOn = false) {
const colors = getColors()
const taskStack = widget.addStack()
taskStack.layoutVertically()
taskStack.spacing = 1
const taskLine = taskStack.addStack()
taskLine.spacing = 4
const circle = taskLine.addText("○")
circle.font = Font.mediumSystemFont(12)
circle.textColor = new Color(colors.circle)
let displayText = parseMarkdownLinks(task.text)
displayText = displayText.length > 50 ? displayText.substring(0, 47) + "..." : displayText
const taskText = taskLine.addText(displayText)
taskText.font = Font.regularSystemFont(12)
taskText.textColor = new Color(colors.text)
taskText.lineLimit = 1
const infoLine = taskStack.addStack()
infoLine.layoutHorizontally()
infoLine.centerAlignContent() // Center align the content vertically
infoLine.spacing = 5
for (const [dateType, date] of Object.entries(task.dates)) {
const { text, color, status } = formatDueDate(date)
const dateStack = infoLine.addStack()
dateStack.centerAlignContent() // Center align the content vertically
dateStack.spacing = 2
const icon = getIconImage(dateType, status)
if (icon) {
const iconElement = dateStack.addImage(icon)
iconElement.imageSize = new Size(10, 10)
iconElement.tintColor = color
} else {
const fallbackText = dateStack.addText(dateType.charAt(0))
fallbackText.font = Font.mediumSystemFont(12)
fallbackText.textColor = color
}
const dateText = dateStack.addText(text)
dateText.font = Font.mediumSystemFont(9)
dateText.textColor = color
infoLine.addSpacer(3)
}
const fileName = getFileName(task.filePath)
if (!fileName.includes('@')) {
infoLine.addSpacer()
const pillStack = infoLine.addStack()
pillStack.centerAlignContent() // Center align the content vertically
pillStack.cornerRadius = 10
pillStack.setPadding(2, 8, 2, 8)
pillStack.backgroundColor = new Color(colors.pill)
const fileNameText = pillStack.addText(fileName)
fileNameText.font = Font.regularSystemFont(9)
fileNameText.textColor = new Color(colors.pillText)
}
if (isWaitingOn) {
infoLine.addSpacer()
const pillStack = infoLine.addStack()
pillStack.centerAlignContent()
pillStack.cornerRadius = 10
pillStack.setPadding(2, 8, 2, 8)
pillStack.backgroundColor = new Color(colors.pill)
const firstName = extractFirstName(task)
const nameText = pillStack.addText(firstName)
nameText.font = Font.regularSystemFont(9)
nameText.textColor = new Color(colors.pillText)
}
const encodedFilePath = encodeURIComponent(task.filePath.replace(vault, ''))
taskStack.url = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodedFilePath}`
}
// Display tasks assigned to me
for (let i = 0; i < Math.min(6, prioritizedAssignedToMe.length); i++) {
addTaskToWidget(prioritizedAssignedToMe[i], widget)
}
widget.addSpacer(4)
const waitingOnStack = widget.addStack()
waitingOnStack.layoutHorizontally()
const waitingOnOthersTitle = waitingOnStack.addText("Waiting On")
waitingOnOthersTitle.font = Font.boldSystemFont(16)
waitingOnOthersTitle.textColor = new Color(colors.accent)
waitingOnStack.addSpacer()
const waitingOnCount = waitingOnStack.addText(`(${totalWaitingOnOthers})`)
waitingOnCount.font = Font.mediumSystemFont(14)
waitingOnCount.textColor = new Color(colors.accent)
widget.addSpacer(4)
// Display tasks waiting on others
for (let i = 0; i < Math.min(3, prioritizedWaitingOnOthers.length); i++) {
addTaskToWidget(prioritizedWaitingOnOthers[i], widget, true)
}
return widget
}
// Helper function to get Unicode icon
function getIconImage(dateType, dueStatus = 'future') {
let imageName
switch (dateType) {
case 'Due':
switch (dueStatus) {
case 'past': imageName = 'flag-red'; break
case 'today': imageName = 'flag-orange'; break
case 'future': imageName = 'flag-green'; break
}
break
case 'Scheduled': imageName = 'calendar'; break
case 'Start': imageName = 'play'; break
case 'End': imageName = 'stop'; break
case 'Repeat': imageName = 'repeat'; break
case 'Date': imageName = 'date'; break
default: imageName = 'pin'
}
const image = loadImage(imageName)
return image || null
}
// Run the script
const widget = await createWidget()
if (config.runsInWidget) {
Script.setWidget(widget)
} else {
widget.presentLarge()
}
Script.complete()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment