Open and Closed "Places" by Hour

Pretty sure there are a few hold overs here from back in the LifeBalance days in the late 90’s If so then here’s some plugin code for you.

This plugin will allow you to tag a subset of tasks with a “place” where that place has open and close hours (defined in the plugin code) when you run the code it will tag and untag the task with “open” or “closed” based on the current time of the day.

This will work on both iOS and MacOs as I have included the URL to call it from a shortcut on iOS.

My use for this is to have 1 set of perspectives that don’t show me tasks that I don’t want to do during certain hours of the day.

The plugin will also create the tags for you so you don’t have to worry about typos between your tags and the code.

The one thing it can’t handle currently is having a place tagged with multiple places; as that can cause things to be flagged with both open and closed.

There is a second plug-in that will toggle everything to “open” on the first run; and on the second run it will toggle them back to the previous status.

OmniFocus Plugins Documentation

Plugin 1: Update Tags Based on Places

Overview
This plugin updates tasks with open or closed tags based on their Places tags and the current time. It ensures tasks are tagged appropriately depending on whether the associated Places tag is ‘open’ or ‘closed’.

Code

/*{
    "author": "Bob",
    "targets": ["omnifocus"],
    "type": "action",
    "identifier": "com.codedaptive.place-hours",
    "version": "1.0",
    "description": "A plug-in that updates tasks with open/closed tags based on place tags and current time.",
    "label": "Update Tags Based on Places 1.0",
    "mediumLabel": "Open-Close 1.0",
    "longLabel": "Update Tags Based on Places 1.0",
    "paletteLabel": "Open-Close 1.0"
}*/
(() => {
    var action = new PlugIn.Action(function(selection) {
        console.log("Invoked Update Tags Based on Places");

        // Define open and close times for each place
        const placeTimes = {
            "office": {
                default: [
                    { open: "08:00", close: "17:00" }
                ],
                overrides: {
                    Monday: [
                        { open: "08:00", close: "17:00" },
                        { open: "16:30", close: "17:00" } // Overlap period
                    ],
                    Tuesday: [
                        { open: "08:00", close: "17:00" },
                        { open: "16:30", close: "17:00" } // Overlap period
                    ],
                    Wednesday: [
                        { open: "08:00", close: "17:00" },
                        { open: "16:30", close: "17:00" } // Overlap period
                    ],
                    Thursday: [
                        { open: "08:00", close: "17:00" },
                        { open: "16:30", close: "17:00" } // Overlap period
                    ],
                    Friday: [
                        { open: "08:00", close: "17:00" },
                        { open: "16:30", close: "17:00" } // Overlap period
                    ]
                }
            },
            "home": {
                default: [
                    { open: "05:00", close: "08:00" },
                    { open: "17:00", close: "23:30" }
                ],
                overrides: {
                    Monday: [
                        { open: "05:00", close: "08:00" },
                        { open: "16:30", close: "23:30" } // Includes overlap period
                    ],
                    Tuesday: [
                        { open: "05:00", close: "08:00" },
                        { open: "16:30", close: "23:30" }
                    ],
                    Wednesday: [
                        { open: "05:00", close: "08:00" },
                        { open: "16:30", close: "23:30" }
                    ],
                    Thursday: [
                        { open: "05:00", close: "08:00" },
                        { open: "16:30", close: "23:30" }
                    ],
                    Friday: [
                        { open: "05:00", close: "08:00" },
                        { open: "16:30", close: "23:30" }
                    ],
                    Saturday: [{ open: "19:00", close: "23:30" }],
                    Sunday: [{ open: "19:00", close: "23:30" }]
                }
            },
            "errands": {
                default: [
                    { open: "07:00", close: "08:00" },
                    { open: "11:30", close: "12:30" },
                    { open: "17:00", close: "19:00" }
                ],
                overrides: {
                    Saturday: [{ open: "07:00", close: "19:00" }],
                    Sunday: [{ open: "07:00", close: "19:00" }]
                }
            },
            "heb": {
                default: [
                    { open: "07:00", close: "23:00" }
                ],
                overrides: {
                    Saturday: [{ open: "07:00", close: "23:00" }],
                    Sunday: [{ open: "07:00", close: "23:00" }]
                }
            },
            "anywhere": {
                default: [
                    { open: "07:00", close: "23:59" }
                ]
            }
        };

        console.log("Place times initialized.");

        // Utility: Parse time strings into timestamps
        const parseTime = (timeString) => {
            const [hours, minutes] = timeString.split(":").map(Number);
            const now = new Date();
            return new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes, 0, 0).getTime();
        };

        // Get the current day and current time
        const now = new Date();
        const currentDay = now.toLocaleString("en-US", { weekday: "long" });
        const currentTime = now.getTime();

        console.log(`Current day: ${currentDay}, Current time: ${now.toTimeString()}`);

        // Ensure a tag exists, creating it if necessary
        const ensureTag = (name, parent = null) => {
            let tag = parent
                ? parent.children.find(t => t.name === name)
                : flattenedTags.find(t => t.name === name);

            if (!tag) {
                tag = parent
                    ? new Tag(name, parent) // Create subtag under parent
                    : new Tag(name); // Create root-level tag
                console.log(
                    parent
                        ? `Created subtag: ${name} under parent: ${parent.name}`
                        : `Created root-level tag: ${name}`
                );
            }
            return tag;
        };

        // Ensure "Places" and "Status" hierarchies exist
        const placesTag = ensureTag("Places");
        const statusTag = ensureTag("Status");
        const openTag = ensureTag("open", statusTag);
        const closedTag = ensureTag("closed", statusTag);

        console.log("Tag hierarchies ensured.");

        // Create or retrieve all place tags under "Places"
        const placeTags = {};
        for (const place of Object.keys(placeTimes)) {
            const placeTag = ensureTag(place, placesTag);
            placeTags[place] = placeTag;
        }

        console.log("All place tags ensured.");

        // Function: Get active intervals for a place
        const getActiveIntervals = (place) => {
            const { default: defaultIntervals, overrides } = placeTimes[place];
            if (overrides && overrides[currentDay] !== undefined) {
                return overrides[currentDay];
            }
            return defaultIntervals;
        };

        // Function: Determine if current time is within any interval
        const isOpenAtCurrentTime = (timeIntervals) => {
            return timeIntervals.some(interval => {
                const openTime = parseTime(interval.open);
                const closeTime = parseTime(interval.close);
                return currentTime >= openTime && currentTime <= closeTime;
            });
        };

        // Process each task
        console.log("Starting task processing...");
        flattenedTasks.forEach(task => {
            const taskTags = task.tags;

            console.log(`Processing task: ${task.name}, Tags: ${taskTags.map(t => t.name).join(", ")}`);

            // Check if the task has any of the place tags
            for (const [placeName, placeTag] of Object.entries(placeTags)) {
                if (taskTags.includes(placeTag)) {
                    const activeIntervals = getActiveIntervals(placeName);
                    const isOpen = isOpenAtCurrentTime(activeIntervals);

                    if (isOpen) {
                        console.log(`Task '${task.name}' place '${placeName}' is OPEN.`);
                        if (!taskTags.includes(openTag)) {
                            task.addTag(openTag);
                            console.log(`Added 'open' tag to task: ${task.name}`);
                        }
                        task.removeTag(closedTag);
                    } else {
                        console.log(`Task '${task.name}' place '${placeName}' is CLOSED.`);
                        if (!taskTags.includes(closedTag)) {
                            task.addTag(closedTag);
                            console.log(`Added 'closed' tag to task: ${task.name}`);
                        }
                        task.removeTag(openTag);
                    }
                }
            }
        });

        console.log("Task processing complete.");
    });

    return action;
})();

Installation

  1. Save the code above as a file named update-tags-based-on-places.omnijs.
  2. Drag the file into OmniFocus on macOS or save it to iCloud for iOS sync.
  3. The plugin will appear under OmniFocus’s Plug-In preferences.
    Shortcuts for macOS and iOS
    To run the plugin via Shortcuts, use the following URL:
    omnifocus:///omnijs-run?script=PlugIn.find%28%27com.codedaptive.place-hours%27%29.actions%5B0%5D.perform%28%29%3B

Plugin 2: Toggle All Open
Overview
This plugin toggles tasks associated with Places tags between the open and closed states. It saves the original state of tasks in their notes to allow for restoration.
Code

/*{
    "author": "Bob",
    "targets": ["omnifocus"],
    "type": "action",
    "identifier": "com.codedaptive.toggle-all-open",
    "version": "1.2",
    "description": "Toggle all tasks between 'open' and their previous states.",
    "label": "Toggle All Open",
    "mediumLabel": "Toggle Open",
    "longLabel": "Toggle All Open or Restore Previous State",
    "paletteLabel": "Toggle Open"
}*/
(() => {
    var action = new PlugIn.Action(function(selection) {
        console.log("Starting toggle between 'all open' and 'restore state'...");

        // Ensure required root tags exist
        let placesTag = flattenedTags.find(t => t.name === "Places");
        if (!placesTag) {
            console.error('Root tag "Places" not found. Aborting.');
            return;
        }

        let statusTag = flattenedTags.find(t => t.name === "Status");
        if (!statusTag) {
            statusTag = new Tag("Status");
            console.log('Created root tag: "Status".');
        }

        let openTag = statusTag.children.find(t => t.name === "open");
        if (!openTag) {
            openTag = new Tag("open", statusTag);
            console.log('Created tag: "open" under "Status".');
        }

        let closedTag = statusTag.children.find(t => t.name === "closed");
        if (!closedTag) {
            closedTag = new Tag("closed", statusTag);
            console.log('Created tag: "closed" under "Status".');
        }

        // Check for the restore marker
        const RESTORE_MARKER = "[State Saved]";
        let hasRestoreMarker = false;

        placesTag.children.forEach(placeTag => {
            console.log(`Processing place: "${placeTag.name}"`);

            placeTag.tasks.forEach(task => {
                if (task.note.includes(RESTORE_MARKER)) {
                    hasRestoreMarker = true;
                }
            });
        });

        if (hasRestoreMarker) {
            // Restore tasks to their original state
            console.log("Restoring tasks to their previous state...");
            placesTag.children.forEach(placeTag => {
                placeTag.tasks.forEach(task => {
                    if (task.note.includes(RESTORE_MARKER)) {
                        let originalState = task.note.match(/\[State: (.*?)\]/)[1];
                        task.note = task.note.replace(`${RESTORE_MARKER} [State: ${originalState}]`, "");

                        // Clear current status
                        task.removeTag(openTag);
                        task.removeTag(closedTag);

                        // Restore original status
                        if (originalState === "open") {
                            task.addTag(openTag);
                        } else if (originalState === "closed") {
                            task.addTag(closedTag);
                        }
                        console.log(`Restored task: "${task.name}" to "${originalState}".`);
                    }
                });
            });
        } else {
            // Save the current state and set all tasks to "open"
            console.log("Saving current state and setting all tasks to 'open'...");

            placesTag.children.forEach(placeTag => {
                placeTag.tasks.forEach(task => {
                    let currentState = "none";
                    if (task.tags.includes(openTag)) currentState = "open";
                    if (task.tags.includes(closedTag)) currentState = "closed";

                    // Save current state in the task note
                    task.note += ` ${RESTORE_MARKER} [State: ${currentState}]`;

                    // Set task to "open"
                    task.removeTag(closedTag);
                    task.addTag(openTag);
                    console.log(`Task "${task.name}" set to "open" with state saved.`);
                });
            });
        }
        console.log("Toggle operation complete.");
    });
    action.validate = function(selection) {
        return true; // Always valid
    };
    return action;
})();

Installation

  1. Save the code above as a file named toggle-all-open.omnijs.
  2. Drag the file into OmniFocus on macOS or save it to iCloud for iOS sync.
  3. The plugin will appear under OmniFocus’s Plug-In preferences.
    Shortcuts for macOS and iOS
    To run the plugin via Shortcuts, use the following URL:
    omnifocus:///omnijs-run?script=PlugIn.find%28%27com.codedaptive.toggle-all-open%27%29.actions%5B0%5D.perform%28%29%3B

This looks very cool!

In case it’s useful to anyone who may happen upon this thread in times to come, an alternative option I work with to achieve something similar is here: GitHub - ksalzke/defer-tag-omnifocus-plugin: Defer Tag (OmniFocus Plugin)

Yours is a bit neater in some ways, but my version does allow for multiple places - sort of - because it puts the tag on hold, if any of the tagged places are unavailable the task will also be unavailable, so there’s no way to specify an ‘or’ condition currently except by using a separate tag. :-)