Skip to content

Instantly share code, notes, and snippets.

@eby
Last active December 2, 2025 15:53
Show Gist options
  • Select an option

  • Save eby/653758759657f3f3314a2fbba7642425 to your computer and use it in GitHub Desktop.

Select an option

Save eby/653758759657f3f3314a2fbba7642425 to your computer and use it in GitHub Desktop.
Scriptable Widget to Show Upcoming Tasks from Craft

Scriptable Widget (Large) for Upcoming Tasks from Craft Docs

This requires the latest Craft that supports API/MCP and the Scriptable app on iOS.

Make sure to edit the configuration settings at the top of the script.

Scriptable is not interactive but you can make it so that when you click on it, it opens Craft tasks view.

Edit the widget, choose open url, and enter craftdocs://openAllTasks

// Tasks Widget for Scriptable
// Displays upcoming tasks from your task management system
// Large widget with interactive checkboxes
// ============ CONFIGURATION ============
const API_BASE_URL = "YOUR_API_BASE_URL"; // Replace with your actual API base URL
const AUTH_TOKEN = "YOUR_AUTH_TOKEN"; // Replace with your authentication token if needed
const MAX_TASKS_TO_SHOW = 15; // Maximum number of tasks to display (large widget can show more)
const SHOW_ACTIVE_TASKS = true; // Show active tasks (due before now)
const SHOW_UPCOMING_TASKS = true; // Show upcoming tasks (scheduled after now)
// ============ API FUNCTIONS ============
async function fetchTasks(scope) {
const request = new Request(`${API_BASE_URL}/tasks?scope=${scope}`);
if (AUTH_TOKEN) {
request.headers = { "Authorization": `Bearer ${AUTH_TOKEN}` };
}
const response = await request.loadJSON();
return response.items || [];
}
async function getTasks() {
try {
let allTasks = [];
// Fetch active tasks (tasks due before now that are not completed/cancelled)
if (SHOW_ACTIVE_TASKS) {
const activeTasks = await fetchTasks("active");
allTasks = allTasks.concat(activeTasks.map(task => ({
...task,
category: "active"
})));
}
// Fetch upcoming tasks (tasks scheduled after now)
if (SHOW_UPCOMING_TASKS) {
const upcomingTasks = await fetchTasks("upcoming");
allTasks = allTasks.concat(upcomingTasks.map(task => ({
...task,
category: "upcoming"
})));
}
// Filter to only show tasks with dates and not completed
const filteredTasks = allTasks.filter(task => {
const scheduleDate = task.taskInfo?.scheduleDate;
const deadlineDate = task.taskInfo?.deadlineDate;
const hasDate = scheduleDate || deadlineDate;
const isNotDone = task.taskInfo?.state !== "done" &&
task.taskInfo?.state !== "canceled" &&
task.taskInfo?.state !== "cancelled";
return hasDate && isNotDone;
});
// Sort tasks by date (prefer schedule date, then deadline)
const sortedTasks = filteredTasks.sort((a, b) => {
const dateA = a.taskInfo?.scheduleDate || a.taskInfo?.deadlineDate;
const dateB = b.taskInfo?.scheduleDate || b.taskInfo?.deadlineDate;
return new Date(dateA) - new Date(dateB);
});
return sortedTasks.slice(0, MAX_TASKS_TO_SHOW);
} catch (error) {
console.error("Error fetching tasks:", error);
return [];
}
}
// ============ WIDGET CREATION ============
function cleanMarkdown(markdown) {
if (!markdown) return "Untitled";
// Remove task checkbox syntax: "- [ ] " or "- [x] " or "- [X] "
return markdown.replace(/^-\s*\[[xX\s]\]\s*/g, "").trim();
}
function formatDate(dateString) {
if (!dateString) return "";
// Parse as local date to avoid timezone issues
const [year, month, day] = dateString.split('-').map(Number);
const date = new Date(year, month - 1, day);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const taskDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const diffDays = Math.floor((taskDate - today) / (1000 * 60 * 60 * 24));
if (diffDays === 0) return "Today";
if (diffDays === 1) return "Tomorrow";
if (diffDays === -1) return "Yesterday";
if (diffDays < -1) return `${Math.abs(diffDays)}d overdue`;
if (diffDays <= 7) return `${diffDays}d`;
const displayMonth = date.getMonth() + 1;
const displayDay = date.getDate();
return `${displayMonth}/${displayDay}`;
}
function isOverdue(dateString) {
if (!dateString) return false;
// Parse as local date to avoid timezone issues
const [year, month, day] = dateString.split('-').map(Number);
const date = new Date(year, month - 1, day);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const taskDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
return taskDate < today;
}
async function createWidget(tasks) {
const widget = new ListWidget();
// Set background gradient
const gradient = new LinearGradient();
gradient.colors = [new Color("#1c1c1e"), new Color("#2c2c2e")];
gradient.locations = [0, 1];
widget.backgroundGradient = gradient;
widget.setPadding(16, 16, 16, 16);
// Header
const header = widget.addStack();
header.layoutHorizontally();
header.centerAlignContent();
const titleText = header.addText("📋 Tasks");
titleText.font = Font.boldSystemFont(18);
titleText.textColor = Color.white();
header.addSpacer();
const countText = header.addText(`${tasks.length}`);
countText.font = Font.semiboldSystemFont(16);
countText.textColor = Color.orange();
widget.addSpacer(12);
// Tasks list
if (tasks.length === 0) {
const emptyText = widget.addText("No upcoming tasks! 🎉");
emptyText.font = Font.systemFont(15);
emptyText.textColor = Color.gray();
emptyText.centerAlignText();
} else {
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i];
const taskStack = widget.addStack();
taskStack.layoutHorizontally();
taskStack.centerAlignContent();
taskStack.spacing = 10;
// Static checkbox (non-interactive)
const checkboxStack = taskStack.addStack();
checkboxStack.size = new Size(20, 20);
checkboxStack.cornerRadius = 4;
checkboxStack.borderWidth = 2;
checkboxStack.borderColor = Color.gray();
checkboxStack.backgroundColor = new Color("#1c1c1e");
// Task content stack
const contentStack = taskStack.addStack();
contentStack.layoutVertically();
contentStack.spacing = 2;
// Task title (cleaned markdown content)
const cleanedMarkdown = cleanMarkdown(task.markdown);
const taskTitle = contentStack.addText(cleanedMarkdown);
taskTitle.font = Font.systemFont(14);
taskTitle.textColor = Color.white();
taskTitle.lineLimit = 2;
taskStack.addSpacer();
// Date stack for both schedule and deadline (in a row)
const scheduleDate = task.taskInfo?.scheduleDate;
const deadlineDate = task.taskInfo?.deadlineDate;
if (scheduleDate || deadlineDate) {
const dateStack = taskStack.addStack();
dateStack.layoutHorizontally();
dateStack.spacing = 8;
dateStack.centerAlignContent();
// Schedule date
if (scheduleDate) {
const scheduleStack = dateStack.addStack();
scheduleStack.layoutHorizontally();
scheduleStack.spacing = 3;
scheduleStack.centerAlignContent();
const scheduleLabel = scheduleStack.addText("📅");
scheduleLabel.font = Font.systemFont(10);
const scheduleDateText = scheduleStack.addText(formatDate(scheduleDate));
scheduleDateText.font = Font.semiboldSystemFont(12);
if (isOverdue(scheduleDate)) {
scheduleDateText.textColor = Color.red();
} else {
// Parse as local date to avoid timezone issues
const [year, month, day] = scheduleDate.split('-').map(Number);
const date = new Date(year, month - 1, day);
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const taskDateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const tomorrowOnly = new Date(tomorrow.getFullYear(), tomorrow.getMonth(), tomorrow.getDate());
if (taskDateOnly <= tomorrowOnly) {
scheduleDateText.textColor = Color.orange();
} else {
scheduleDateText.textColor = Color.gray();
}
}
}
// Deadline date
if (deadlineDate) {
const deadlineStack = dateStack.addStack();
deadlineStack.layoutHorizontally();
deadlineStack.spacing = 3;
deadlineStack.centerAlignContent();
const deadlineLabel = deadlineStack.addText("‼️");
deadlineLabel.font = Font.systemFont(10);
const deadlineDateText = deadlineStack.addText(formatDate(deadlineDate));
deadlineDateText.font = Font.semiboldSystemFont(12);
if (isOverdue(deadlineDate)) {
deadlineDateText.textColor = Color.red();
} else {
// Parse as local date to avoid timezone issues
const [year, month, day] = deadlineDate.split('-').map(Number);
const date = new Date(year, month - 1, day);
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const taskDateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const tomorrowOnly = new Date(tomorrow.getFullYear(), tomorrow.getMonth(), tomorrow.getDate());
if (taskDateOnly <= tomorrowOnly) {
deadlineDateText.textColor = Color.orange();
} else {
deadlineDateText.textColor = Color.gray();
}
}
}
}
if (i < tasks.length - 1) {
widget.addSpacer(8);
}
}
}
widget.addSpacer();
// Last updated timestamp
const updateTime = new Date();
const timeFormatter = new DateFormatter();
timeFormatter.dateFormat = "h:mm a";
const footer = widget.addText(`Updated ${timeFormatter.string(updateTime)}`);
footer.font = Font.systemFont(10);
footer.textColor = new Color("#666666");
footer.centerAlignText();
return widget;
}
// ============ MAIN EXECUTION ============
async function run() {
try {
const tasks = await getTasks();
const widget = await createWidget(tasks);
if (config.runsInWidget) {
Script.setWidget(widget);
} else {
widget.presentLarge();
}
Script.complete();
} catch (error) {
console.error("Widget error:", error);
// Create error widget
const errorWidget = new ListWidget();
errorWidget.backgroundColor = new Color("#1c1c1e");
const errorText = errorWidget.addText("❌ Error loading tasks");
errorText.font = Font.boldSystemFont(14);
errorText.textColor = Color.red();
errorWidget.addSpacer(4);
const detailText = errorWidget.addText(error.toString());
detailText.font = Font.systemFont(10);
detailText.textColor = Color.gray();
if (config.runsInWidget) {
Script.setWidget(errorWidget);
} else {
errorWidget.presentLarge();
}
Script.complete();
}
}
await run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment