- Copy the script under Code section of this document to a new script in Scriptable app.
- Run the script first which should prompt Scriptable to ask for calendar access.
- if it didn't and you haven't given Scriptable calendar access before, try changing the
debug
variable totrue
and trying again. - to have the widget open the iOS calendar app, switch
debug
back tofalse
afterwards.
- if it didn't and you haven't given Scriptable calendar access before, try changing the
- Add a medium sized Scriptable widget to your homescreen.
- Long press the widget and choose "Edit Widget".
- Set the
Script
to be the script you just created andWhen Interacting
toRun Script
which will then launch Calendar app when you tap on the widget. - Return to your home screen which should now hopefully show the Scriptable calendar widget.
- the beginning of the script shows the hex colors for various parts which can be modified to your liking:
- background color
- current day color
- text color
- To get an image that can then be used to have a "transparent" widget background use this script and save it to the Scriptable folder on iCloud. Then set either the widget parameter (long press on the widget -> edit widget -> parameter) to
{ "bg": "my-image.jpg"}
wheremy-image
is the name of your transparent backgorund OR change the line which has{ bg: "1121.jpg" }
to include your image name.
The script also supports small widgets or a medium widget with just one part. In this case the the widget parameter (long press on the widget -> edit widget -> parameter) should be set to something like:
{ "bg": "top-left.jpg", "view": "events" }
{ "bg": "top-right.jpg", "view": "cal" }
Where "events"
specifies the events view and "cal"
the calendar view. (Setting the background is optional).
You are free to use and modify the code however you wish. There are likely going to be bugs in this code as I have not tested it for every edge case.
// use true to initially give Scriptable calendar access
// use false to open Calendar when script is run - when tapping on the widget
const debug = false;
// get widget params
const params = JSON.parse(args.widgetParameter) || { bg: "1121.jpg" };
// a separate image can be specified per widget in widget params:
// Long press on widget -> Edit Widget -> Parameter
// parameter config would look like this:
// { "bg": "2111.jpg", "view": "events" }
const imageName = params.bg;
const backgroundColor = "#000000";
const currentDayColor = "#000000";
const textColor = "#ffffff";
// opacity value for weekends and times
const opacity = 0.7;
// choose either a split view or show only one of them
const showEventsView = params.view ? params.view === "events" : true;
const showCalendarView = params.view ? params.view === "cal" : true;
if (config.runsInWidget) {
let widget = await createWidget();
Script.setWidget(widget);
Script.complete();
} else if (debug) {
Script.complete();
let widget = await createWidget();
await widget.presentMedium();
} else {
const appleDate = new Date("2001/01/01");
const timestamp = (new Date().getTime() - appleDate.getTime()) / 1000;
console.log(timestamp);
const callback = new CallbackURL("calshow:" + timestamp);
callback.open();
Script.complete();
}
async function createWidget() {
let widget = new ListWidget();
widget.backgroundColor = new Color(backgroundColor);
setWidgetBackground(widget, imageName);
widget.setPadding(16, 16, 16, 16);
// layout horizontally
const globalStack = widget.addStack();
if (showEventsView) {
await buildEventsView(globalStack);
}
if (showCalendarView) {
buildCalendarView(globalStack);
}
return widget;
}
/**
* Builds the events view
*
* @param {WidgetStack} stack - onto which the events view is built
*/
async function buildEventsView(stack) {
const leftStack = stack.addStack();
// push event view to the left
stack.addSpacer();
leftStack.layoutVertically();
// center the whole left part of the widget
leftStack.addSpacer();
const date = new Date();
// Find future events that aren't all day and aren't canceled
const events = await CalendarEvent.today([]);
const futureEvents = [];
for (const event of events) {
if (
event.startDate.getTime() > date.getTime() &&
!event.isAllDay &&
!event.title.startsWith("Canceled:")
) {
futureEvents.push(event);
}
}
// if we have events today; else if we don't
if (futureEvents.length !== 0) {
// show the next 3 events at most
const numEvents = futureEvents.length > 3 ? 3 : futureEvents.length;
for (let i = 0; i < numEvents; i += 1) {
formatEvent(leftStack, futureEvents[i], textColor, opacity);
// don't add a spacer after the last event
if (i < numEvents - 1) {
leftStack.addSpacer(8);
}
}
} else {
addWidgetTextLine(leftStack, "No more events today", {
color: textColor,
opacity,
font: Font.regularSystemFont(15),
align: "left",
});
}
// for centering
leftStack.addSpacer();
}
/**
* Builds the calendar view
*
* @param {WidgetStack} stack - onto which the calendar is built
*/
function buildCalendarView(stack) {
const rightStack = stack.addStack();
rightStack.layoutVertically();
const date = new Date();
const dateFormatter = new DateFormatter();
dateFormatter.dateFormat = "MMMM";
// if calendar is on a small widget make it a bit smaller to fit
const spacing = config.widgetFamily === "small" ? 18 : 20;
// Current month line
const monthLine = rightStack.addStack();
// since dates are centered in their squares we need to add some space
monthLine.addSpacer(4);
addWidgetTextLine(monthLine, dateFormatter.string(date).toUpperCase(), {
color: textColor,
textSize: 14,
font: Font.boldSystemFont(13),
});
// between the month name and the week calendar
rightStack.addSpacer(5);
const calendarStack = rightStack.addStack();
calendarStack.spacing = 2;
const month = buildMonthVertical();
for (let i = 0; i < month.length; i += 1) {
let weekdayStack = calendarStack.addStack();
weekdayStack.layoutVertically();
for (let j = 0; j < month[i].length; j += 1) {
let dayStack = weekdayStack.addStack();
dayStack.size = new Size(spacing, spacing);
dayStack.centerAlignContent();
if (month[i][j] === date.getDate().toString()) {
const highlightedDate = getHighlightedDate(
date.getDate().toString(),
currentDayColor
);
dayStack.addImage(highlightedDate);
} else {
addWidgetTextLine(dayStack, `${month[i][j]}`, {
color: textColor,
opacity: i > 4 ? opacity : 1,
font: Font.boldSystemFont(10),
align: "center",
});
}
}
}
}
/**
* Creates an array of arrays, where the inner arrays include the same weekdays
* along with an identifier in 0 position
* [
* [ 'M', ' ', '7', '14', '21', '28' ],
* [ 'T', '1', '8', '15', '22', '29' ],
* [ 'W', '2', '9', '16', '23', '30' ],
* ...
* ]
*
* @returns {Array<Array<string>>}
*/
function buildMonthVertical() {
const date = new Date();
const firstDayStack = new Date(date.getFullYear(), date.getMonth(), 1);
const lastDayStack = new Date(date.getFullYear(), date.getMonth() + 1, 0);
const month = [["M"], ["T"], ["W"], ["T"], ["F"], ["S"], ["S"]];
let dayStackCounter = 0;
for (let i = 1; i < firstDayStack.getDay(); i += 1) {
month[i - 1].push(" ");
dayStackCounter = (dayStackCounter + 1) % 7;
}
for (let date = 1; date <= lastDayStack.getDate(); date += 1) {
month[dayStackCounter].push(`${date}`);
dayStackCounter = (dayStackCounter + 1) % 7;
}
const length = month.reduce(
(acc, dayStacks) => (dayStacks.length > acc ? dayStacks.length : acc),
0
);
month.forEach((dayStacks, index) => {
while (dayStacks.length < length) {
month[index].push(" ");
}
});
return month;
}
/**
* Draws a circle with a date on it for highlighting in calendar view
*
* @param {string} date to draw into the circle
*
* @returns {Image} a circle with the date
*/
function getHighlightedDate(date) {
const drawing = new DrawContext();
drawing.respectScreenScale = true;
const size = 50;
drawing.size = new Size(size, size);
drawing.opaque = false;
drawing.setFillColor(new Color("#000000"));
drawing.fillEllipse(new Rect(1, 1, size - 2, size - 2));
drawing.setFont(Font.boldSystemFont(25));
drawing.setTextAlignedCenter();
drawing.setTextColor(new Color("#ffffff"));
drawing.drawTextInRect(date, new Rect(0, 10, size, size));
const currentDayImg = drawing.getImage();
return currentDayImg;
}
/**
* formats the event times into just hours
*
* @param {Date} date
*
* @returns {string} time
*/
function formatTime(date) {
let dateFormatter = new DateFormatter();
dateFormatter.useNoDateStyle();
dateFormatter.useShortTimeStyle();
return dateFormatter.string(date);
}
/**
* Adds a event name along with start and end times to widget stack
*
* @param {WidgetStack} stack - onto which the event is added
* @param {CalendarEvent} event - an event to add on the stack
* @param {number} opacity - text opacity
*/
function formatEvent(stack, event, color, opacity) {
addWidgetTextLine(stack, event.title, {
color,
font: Font.mediumSystemFont(14),
lineLimit: 1,
});
// create line for event start and end times
let timeStack = stack.addStack();
const time = `${formatTime(event.startDate)} - ${formatTime(event.endDate)}`;
addWidgetTextLine(timeStack, time, {
color,
opacity,
font: Font.regularSystemFont(14),
});
}
function addWidgetTextLine(
widget,
text,
{
color = "#ffffff",
textSize = 12,
opacity = 1,
align,
font = "",
lineLimit = 0,
}
) {
let textLine = widget.addText(text);
textLine.textColor = new Color(color);
if (typeof font === "string") {
textLine.font = new Font(font, textSize);
} else {
textLine.font = font;
}
console.log(`${text}`);
console.log(`${typeof opacity}`);
textLine.textOpacity = opacity;
switch (align) {
case "left":
textLine.leftAlignText();
break;
case "center":
textLine.centerAlignText();
break;
case "right":
textLine.rightAlignText();
break;
default:
textLine.leftAlignText();
break;
}
}
function getImageUrl(name) {
let fm = FileManager.iCloud();
let dir = fm.documentsDirectory();
return fm.joinPath(dir, `${name}`);
}
function setWidgetBackground(widget, imageName) {
const imageUrl = getImageUrl(imageName);
console.log(imageUrl);
widget.backgroundImage = Image.fromFile(imageUrl);
}