├── .github └── workflows │ └── main.yml ├── CHANGELOG.md ├── README.md ├── Run OF Plug-In Action - Update Schedule.kmmacros └── Scheduling.omnifocusjs ├── Resources ├── en.lproj │ ├── manifest.strings │ ├── preferences.strings │ ├── rescheduleTask.strings │ ├── unscheduleTask.strings │ └── updateSchedule.strings ├── preferences.js ├── rescheduleTask.js ├── schedulingLib.js ├── unscheduleTask.js └── updateSchedule.js └── manifest.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | - name: Conventional Changelog Action 29 | id: changelog 30 | uses: TriPSs/conventional-changelog-action@v3 31 | with: 32 | version-file: Scheduling.omnifocusjs/manifest.json 33 | release-count: 0 34 | 35 | - name: Release 36 | uses: softprops/action-gh-release@v1 37 | if: ${{ steps.changelog.outputs.skipped == 'false' }} 38 | with: 39 | tag_name: ${{ steps.changelog.outputs.tag }} 40 | body: ${{ steps.changelog.outputs.clean_changelog }} 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.5.0](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.4.0...v2.5.0) (2023-08-30) 2 | 3 | 4 | ### Features 5 | 6 | * :children_crossing: use default scheduled time instead of default due time ([987972e](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/987972ee02649c48e5cd7bd553a0368cd67df345)), closes [#12](https://github.com/ksalzke/scheduling-omnifocus-plugin/issues/12) 7 | 8 | 9 | 10 | # [2.4.0](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.3.1...v2.4.0) (2023-08-26) 11 | 12 | 13 | ### Features 14 | 15 | * :sparkles: add getScheduleInfo to return string containing scheduled dates ([631f3ce](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/631f3ce66ac645658b8345344f013317549fb9e7)) 16 | 17 | 18 | 19 | ## [2.3.1](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.3.0...v2.3.1) (2023-08-13) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * :bug: fix getTag logic when searching for date string, fixes [#11](https://github.com/ksalzke/scheduling-omnifocus-plugin/issues/11) ([2711211](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/2711211e8c21d4e2c59fa2459068247b818b4f07)) 25 | 26 | 27 | 28 | # [2.3.0](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.2.1...v2.3.0) (2023-06-17) 29 | 30 | 31 | ### Features 32 | 33 | * :sparkles: allow unscheduling of projects, not just tasks ([c8fb05d](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/c8fb05dba5ac2188583baf5bd570f80ed7ae26d7)) 34 | 35 | 36 | 37 | ## [2.2.1](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.2.0...v2.2.1) (2023-06-04) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * :bug: fix bug where tags were not removed when unscheduling, and unscheduling stopped at first task ([7d47b43](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/7d47b43d39a06f45f5e3f7706f06708711fa9b36)) 43 | 44 | 45 | 46 | # [2.2.0](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.1.0...v2.2.0) (2023-01-31) 47 | 48 | 49 | ### Features 50 | 51 | * :sparkles: enable plug-in to act on projects, not just tasks ([147a732](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/147a73296b663d4cc840a02d07c149a3f09847ea)), closes [#9](https://github.com/ksalzke/scheduling-omnifocus-plugin/issues/9) 52 | 53 | 54 | 55 | # [2.1.0](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.0.2...v2.1.0) (2022-11-18) 56 | 57 | 58 | ### Features 59 | 60 | * :sparkles: add 'unschedule task(s)' action ([bce920f](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/bce920ff40ca0222aafbad7d6a5c4e51ffaefcc0)) 61 | * :sparkles: add option to 'add scheduled notification' for displaying in forecast ([60a595b](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/60a595b2809d333c2116d7db1d3288a8fcf60e63)) 62 | 63 | 64 | 65 | ## [2.0.2](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.0.1...v2.0.2) (2022-11-05) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * :lipstick: correct label for Preferences dialogue ([cb8acba](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/cb8acba1d4dbc18567115e05c44d37fdb2d54f21)) 71 | 72 | 73 | 74 | ## [2.0.1](https://github.com/ksalzke/scheduling-omnifocus-plugin/compare/v2.0.0...v2.0.1) (2022-04-07) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * :bug: fix bug where today tag was not removed when task rescheduled (see issue [#2](https://github.com/ksalzke/scheduling-omnifocus-plugin/issues/2)) ([9c1c906](https://github.com/ksalzke/scheduling-omnifocus-plugin/commit/9c1c9066bb66e7788ec25e9c27071fb097beb0e4)) 80 | 81 | 82 | 83 | # 2.0.0 (2022-03-02) 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This is an OmniFocus plug-in that assigns a 'do date' to tasks using a system of tags. 4 | 5 | _Please note that all scripts on my GitHub account (or shared elsewhere) are works in progress. If you encounter any issues or have any suggestions please let me know--and do please make sure you backup your database before running scripts from the internet!)_ 6 | 7 | ## Known issues 8 | 9 | Refer to ['issues'](https://github.com/ksalzke/scheduling-omnifocus-plugin/issues) for known issues and planned changes/enhancements. 10 | 11 | # Installation & Set-Up 12 | 13 | ## Synced Preferences Plug-In 14 | 15 | **Important note: for this plug-in bundle to work correctly, my [Synced Preferences for OmniFocus plug-in](https://github.com/ksalzke/synced-preferences-for-omnifocus) is also required and needs to be added to the plug-in folder separately.** 16 | 17 | ## Installation 18 | 19 | 1. Download the [latest release](https://github.com/ksalzke/scheduling-omnifocus-plugin/releases/latest). 20 | 2. Unzip the downloaded file. 21 | 3. Move the `.omnifocusjs` file to your OmniFocus plug-in library folder (or open it to install). 22 | 4. If desired, configure your preferences using the `Preferences` action. 23 | 24 | ## Set-Up 25 | 26 | This plug-in utilises a series of tags which are nested under a user-specified 'Scheduling' tag, which may be placed anywhere in the tag hierarchy and can have any name. This tag should be created manually, and then set in the preferences. 27 | 28 | Optionally, a 'Today' tag can also be used. 29 | 30 | # Actions 31 | 32 | This plug-in contains the following actions: 33 | 34 | ## (Re)schedule Task(s) 35 | 36 | This action can be run when one or more tasks are selected. 37 | 38 | It prompts the user to enter a date (which may use Omni's [shorthand date terminology](https://omni-automation.com/shared/formatter-date.html)) and 'reschedules' the task to that date by assigning a tag. 39 | 40 | ![Rescheduling a task](https://user-images.githubusercontent.com/16893787/155266319-deff7fd7-b6a2-4a4a-a94e-4dd7221b0bc3.png) 41 | 42 | ## Unschedule Task(s) 43 | 44 | This action can be run when one or more tasks are selected. 45 | 46 | It 'unschedules' the selected tasks, based on the settings in preferences. 47 | 48 | ## Update Schedule 49 | 50 | This action can be run at any time. You may wish to run this in the background on a daily basis using the included Keyboard Maestro macro. 51 | 52 | It runs the `updateTags()` function from the library (described in detail below), which moves any tasks scheduled for today (or the past) to today, by applying a flag and/or assigning a tag (as determined by the user's preferences.) It also updates the tag order and removes any tags that are more than a week in the future and do not have any remaining tasks assigned to them. 53 | 54 | ![Ordered tags](https://user-images.githubusercontent.com/16893787/155266369-6d5c59a7-c0bd-46d0-a104-ddfbb86abec1.png) 55 | 56 | ## Preferences: Scheduling 57 | 58 | This action allows the user to set the preferences for the plug-in. These sync between devices using the Synced Preferences plug-in linked above. 59 | 60 | The following preferences are available: 61 | 62 | * **Scheduling tag:** This is the root tag under which all 'Scheduling' tags are created by the plug-in. It can be named anything and located anywhere in the tag hierarchy. 63 | * **'Today' tag:** A tag that denotes tasks that have been scheduled for Today. This is optional. If 'None' is selected, no today tag is used. 64 | * **Flag denotes 'Today' tasks.** If selected, flags are used to represent 'Today' tasks. Note that if a task is rescheduled from today and this checkbox is selected, the flag will be _removed_ from the task. 65 | * **Use scheduled notifications.** If selected, a notification will be added to the task on the chosen date. (Note that any existing notifications will be removed first.) This allows the task to show in the forecast view if 'Items with scheduled notifications' is ticked. 66 | * **Use recurring weekday-based scheduling.** If selected, tags for each day of the week (e.g. 'Wednesdays', 'Thursdays' and 'Fridays') will also be created, which can contain repeating tasks. These will be marked as 'Today' each week on that day, provided the defer date of the task is not _after_ the specified day. (e.g. if a task is tagged with 'Wednesdays' but is deferred until Thursday, it will be assumed the task has been completed early and won't be marked 'Today') 67 | 68 | # Functions 69 | 70 | This plug-in contains the following functions within the `schedulingLib` library. 71 | 72 | ## `loadSyncedPrefs () : SyncedPref` 73 | 74 | Returns the [SyncedPref](https://github.com/ksalzke/synced-preferences-for-omnifocus) object for this plug-in. 75 | 76 | If the user does not have the plug-in installed correctly, they are alerted. 77 | 78 | ## `todayTag () : Tag | null` 79 | 80 | Returns the 'Today' tag set in preferences. If no tag has been set, or the tag no longer exists, returns null. 81 | 82 | ## `getDateFormatter () : Formatter.Date` 83 | 84 | Returns the Omni Automation date formatter that is used to format the date strings. This uses the user’s “medium” format as selected in system settings. 85 | 86 | ## `getDateString (date: Date) : String` 87 | 88 | Returns a string for the given date, based on the user's "medium" format selected in system settings. 89 | 90 | ## `daysFromToday (date: Date) : Number` 91 | 92 | Returns the number of days between today and the given date. 93 | 94 | If the date is more than a month away, return Infinity. (In this context of this plug-in, we only care about values within a week.) 95 | 96 | ## `getDayOfWeek (date: Date) : String` 97 | 98 | Returns the day of the week for the given date. 99 | 100 | ## `getWeekdayTag (date: Date) : Tag` 101 | 102 | **Asynchronous.** Returns the 'weekday' tag for the given day of the week. e.g. if a Wednesday date is passed, returns (and creates if necessary) the scheduling tag named 'Wednesdays'. 103 | 104 | ## `getString (date: Date) : String | null` 105 | 106 | Returns a string that is used as the tag name for the given date. This uses the user's "medium" format as selected in system settings, and depends on how far in the future the date is. 107 | 108 | If the date is today, returns null. 109 | If the date is tomorrow returns 'Tomorrow' and the date e.g. 'Tomorrow (24 Feb 2022)' 110 | If the date is after tomorrow but in the next seven days, returns the weekday followed by the date e.g. 'Sunday (27 Feb 2022)'. 111 | If the date is not in the next seven days, returns the date e.g. '4 Mar 2022'. 112 | 113 | ## `isAfterToday (date: Date) : Boolean` 114 | 115 | Returns true if the given date is after today; otherwise returns false. 116 | 117 | ## `schedulingTag () : Tag | null` 118 | 119 | Returns the 'Scheduling' tag set in preferences. If no tag has been set, or the tag no longer exists, returns null. 120 | 121 | ## `getSchedulingTag () : Tag` 122 | 123 | **Asynchronous.** Returns the 'Scheduling' tag set in preferences. If no tag has been set, or the tag no longer exists, displays the preferences pane. 124 | 125 | ## `createTag (date: Date) : Tag` 126 | 127 | **Asynchronous.** Creates a tag for the provided date and re-orders the scheduling tags as needed. 128 | 129 | ## `getTag (date: Date) : Tag` 130 | 131 | **Asynchronous.** Returns the tag for the provided date, if it exists. If no tag exists for the given date, uses `createTag` to create one. 132 | 133 | ## `getDate (tag: Tag) : Date | null` 134 | 135 | Returns the date for a given tag. If no date can be parsed, returns null. 136 | 137 | ## `getDateStringFromTag (tag: Tag) : String` 138 | 139 | Returns the date string for a given tag. 140 | 141 | ## `isToday (date: Date) : Boolean` 142 | 143 | Returns true if the given date is today; otherwise, returns false. 144 | 145 | ## `promptAndReschedule (tasks: Array)` 146 | 147 | **Asynchronous.** 'Reschedules' the given task to the specified date by removing any existing 'scheduling' tags, and adding the tag for the given day. 148 | 149 | Prompts the user to enter a date (which may use Omni's [shorthand date terminology](https://omni-automation.com/shared/formatter-date.html)) and 'reschedules' the task to that date by assigning a tag. 150 | 151 | ![Rescheduling a task](https://user-images.githubusercontent.com/16893787/155266319-deff7fd7-b6a2-4a4a-a94e-4dd7221b0bc3.png) 152 | 153 | ## `rescheduleTask (task: Task, date: Date)` 154 | 155 | **Asynchronous.** 'Reschedules' the given task to the specified date by removing any existing 'scheduling' tags, and adding the tag for the given day. 156 | 157 | Depending on the user's preferences, a notification will also be added (and existing notifications removed) and/or a flag added to tasks scheduled to today. 158 | 159 | ## `unscheduleTasks (tasks: Array)` 160 | 161 | **Asynchronous.** 'Unschedules' the given task by removing any existing scheduling tags, and (depeding on preferences) the flag or scheduled notifications. 162 | 163 | ## `addToToday (task: Task)` 164 | 165 | Moves a task to today by flagging and/or tagging with the 'Today' tag as specified in preferences. 166 | 167 | ## `makeToday (tag: Tag)` 168 | 169 | Moves all of the tasks from a tag to today by running `addToToday` on each of its tasks, and deleting the tag. 170 | 171 | ## `recreateTagOrder ()` 172 | 173 | **Asynchronous.** Re-orders the scheduling tags (and renames them if necessary), in an order similar to that shown at the 'Update Tags' action above. 174 | 175 | ## `updateTags ()` 176 | 177 | **Asynchronous.** This function: 178 | 1. moves any past tags to 'Today' (using `makeToday`), 179 | 180 | 2. removes any future date tags (more than a week in the future) that have no remaining tasks, 181 | 182 | 3. if the option is selected in preferences, schedules any relevant 'weekdays' tasks for today, and 183 | 184 | 4. re-orders the scheduling tags using `recreateTagOrder`. 185 | 186 | ## `getScheduleInfo (task: Task) : string` 187 | 188 | **Asynchronous.** This function takes a task as input and returns details about existing scheduling information as a string. e.g. `Tomorrow (24 Aug 2023)` or `Fridays` or `Tomorrow (24 Aug 2023) and Fridays`. -------------------------------------------------------------------------------- /Run OF Plug-In Action - Update Schedule.kmmacros: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Activate 7 | Normal 8 | CreationDate 9 | 653791836.76091897 10 | Macros 11 | 12 | 13 | Actions 14 | 15 | 16 | Action 17 | PercentEncodeForURL 18 | ActionUID 19 | 11056 20 | Destination 21 | Variable 22 | DestinationVariable 23 | Script 24 | MacroActionType 25 | Filter 26 | Source 27 | Text 28 | Text 29 | PlugIn.find('com.KaitlinSalzke.Scheduling').action('updateSchedule').perform() 30 | 31 | 32 | ActionUID 33 | 11057 34 | MacroActionType 35 | SetVariableToText 36 | Text 37 | omnifocus://localhost/omnijs-run?script=%Script% 38 | Variable 39 | URL 40 | 41 | 42 | ActionUID 43 | 11058 44 | IsDefaultApplication 45 | 46 | MacroActionType 47 | OpenURL 48 | TimeOutAbortsMacro 49 | 50 | URL 51 | %URL% 52 | 53 | 54 | CreationDate 55 | 667284795.66531801 56 | ModificationDate 57 | 667284836.07374895 58 | Name 59 | Run OF Plug-In Action: Update Schedule 60 | Triggers 61 | 62 | 63 | ExecuteType 64 | Time 65 | MacroTriggerType 66 | Time 67 | Repeat 68 | 69 | RepeatTime 70 | 600 71 | TimeFinishHour 72 | 23 73 | TimeFinishMinutes 74 | 59 75 | TimeHour 76 | 0 77 | TimeMinutes 78 | 5 79 | WhichDays 80 | 127 81 | 82 | 83 | UID 84 | 644F59A4-976F-494C-BCF1-49D1CB8F5E9C 85 | 86 | 87 | Name 88 | Temp 89 | ToggleMacroUID 90 | C91D1C44-E5CC-4488-9DEA-30A44E7B99A8 91 | UID 92 | 433D9C71-7DCD-4A50-B5A8-A0A5D680BAE4 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.KaitlinSalzke.Scheduling" = "Scheduling"; -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/en.lproj/preferences.strings: -------------------------------------------------------------------------------- 1 | "label" = "Preferences: Scheduling"; 2 | "shortLabel" = "Preferences: Scheduling"; 3 | "mediumLabel" = "Preferences: Scheduling"; 4 | "longLabel" = "Preferences: Scheduling"; -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/en.lproj/rescheduleTask.strings: -------------------------------------------------------------------------------- 1 | "label" = "(Re)schedule Task(s)"; 2 | "shortLabel" = "(Re)schedule Task(s)"; 3 | "mediumLabel" = "(Re)schedule Task(s)"; 4 | "longLabel" = "(Re)schedule Task(s)"; -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/en.lproj/unscheduleTask.strings: -------------------------------------------------------------------------------- 1 | "label" = "Unschedule Task(s)"; 2 | "shortLabel" = "Unschedule Task(s)"; 3 | "mediumLabel" = "Unschedule Task(s)"; 4 | "longLabel" = "Unschedule Task(s)"; -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/en.lproj/updateSchedule.strings: -------------------------------------------------------------------------------- 1 | "label" = "Update Schedule"; 2 | "shortLabel" = "Update Schedule"; 3 | "mediumLabel" = "Update Schedule"; 4 | "longLabel" = "Update Schedule"; -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/preferences.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Form flattenedTags */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | const syncedPrefs = this.schedulingLib.loadSyncedPrefs() 5 | 6 | // get current preferences or set defaults if they don't yet exist 7 | const schedulingTag = this.schedulingLib.schedulingTag() 8 | const todayTag = this.schedulingLib.todayTag() 9 | const flagToday = syncedPrefs.readBoolean('flagToday') 10 | const useScheduledNotifications = syncedPrefs.readBoolean('useScheduledNotifications') 11 | const useWeekdays = syncedPrefs.readBoolean('useWeekdays') 12 | 13 | // create and show form 14 | const form = new Form() 15 | const tagNames = flattenedTags.map(t => t.name) 16 | 17 | form.addField(new Form.Field.Option('schedulingTag', 'Scheduling tag', flattenedTags, tagNames, schedulingTag, 'Please select a tag')) 18 | 19 | const todayTagField = new Form.Field.Option('todayTag', '\'Today\' tag', flattenedTags, tagNames, todayTag, 'None') 20 | todayTagField.allowsNull = true 21 | form.addField(todayTagField) 22 | 23 | form.addField(new Form.Field.Checkbox('flagToday', 'Flag denotes \'Today\' tasks', flagToday)) 24 | form.addField(new Form.Field.Checkbox('useScheduledNotifications', 'Use scheduled notifications', useScheduledNotifications)) 25 | 26 | form.addField(new Form.Field.Checkbox('useWeekdays', 'Use recurring weekday-based scheduling', useWeekdays)) 27 | 28 | await form.show('Preferences: Scheduling', 'OK') 29 | 30 | // save preferences 31 | if (form.values.todayTag) syncedPrefs.write('todayTagID', form.values.todayTag.id.primaryKey) 32 | else syncedPrefs.write('todayTagID', null) 33 | syncedPrefs.write('flagToday', form.values.flagToday) 34 | syncedPrefs.write('useScheduledNotifications', form.values.useScheduledNotifications) 35 | syncedPrefs.write('schedulingTagID', form.values.schedulingTag.id.primaryKey) 36 | syncedPrefs.write('useWeekdays', form.values.useWeekdays) 37 | }) 38 | 39 | action.validate = function (selection, sender) { 40 | return true 41 | } 42 | 43 | return action 44 | })() 45 | -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/rescheduleTask.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Form */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | 5 | this.schedulingLib.promptAndReschedule([...selection.tasks, ...selection.projects.map(p => p.task)]) 6 | }) 7 | 8 | action.validate = function (selection, sender) { 9 | return [...selection.tasks, ...selection.projects].length > 0 10 | } 11 | 12 | return action 13 | })() 14 | -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/schedulingLib.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Version Formatter Tag Calendar moveTags deleteObject Alert DateComponents */ 2 | (() => { 3 | const schedulingLib = new PlugIn.Library(new Version('1.0')) 4 | 5 | schedulingLib.loadSyncedPrefs = () => { 6 | const syncedPrefsPlugin = PlugIn.find('com.KaitlinSalzke.SyncedPrefLibrary') 7 | 8 | if (syncedPrefsPlugin !== null) { 9 | const SyncedPref = syncedPrefsPlugin.library('syncedPrefLibrary').SyncedPref 10 | return new SyncedPref('com.KaitlinSalzke.Scheduling') 11 | } else { 12 | const alert = new Alert( 13 | 'Synced Preferences Library Required', 14 | 'For the Scheduling plug-in to work correctly, the \'Synced Preferences for OmniFocus\' plug-in (https://github.com/ksalzke/synced-preferences-for-omnifocus) is also required and needs to be added to the plug-in folder separately. Either you do not currently have this plugin installed, or it is not installed correctly.' 15 | ) 16 | alert.show() 17 | } 18 | } 19 | 20 | schedulingLib.todayTag = () => { 21 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 22 | const todayTagID = syncedPrefs.read('todayTagID') 23 | if (todayTagID === null) return null 24 | else return Tag.byIdentifier(todayTagID) 25 | } 26 | 27 | schedulingLib.getDateFormatter = () => { 28 | return Formatter.Date.withStyle(Formatter.Date.Style.Medium) 29 | } 30 | 31 | schedulingLib.getDateString = (date) => { 32 | const formatter = schedulingLib.getDateFormatter() 33 | return formatter.stringFromDate(date) 34 | } 35 | 36 | schedulingLib.daysFromToday = (date) => { 37 | const startOfToday = Calendar.current.startOfDay(new Date()) 38 | const startOfDate = Calendar.current.startOfDay(date) 39 | const components = Calendar.current.dateComponentsBetweenDates(startOfToday, startOfDate) 40 | 41 | // if the date is more than a month away, return Infinity 42 | // in this context, we only care about values within a week 43 | if (components.month > 0 || components.year > 0 || components.era > 0) { 44 | return Infinity 45 | } 46 | 47 | return components.day 48 | } 49 | 50 | schedulingLib.getDayOfWeek = (date) => { 51 | const dayFormatter = Formatter.Date.withFormat('EEEE') 52 | return dayFormatter.stringFromDate(date) 53 | } 54 | 55 | schedulingLib.getWeekdayTag = async (date) => { 56 | const tagName = `${schedulingLib.getDayOfWeek(date)}s` 57 | const schedulingTag = await schedulingLib.getSchedulingTag() 58 | return schedulingTag.tagNamed(tagName) || new Tag(tagName, schedulingTag) 59 | } 60 | 61 | schedulingLib.getString = (date) => { 62 | const dateString = schedulingLib.getDateString(date) 63 | const daysFromToday = schedulingLib.daysFromToday(date) 64 | if (daysFromToday > 7) return dateString 65 | if (daysFromToday === 1) return `Tomorrow (${dateString})` 66 | if (daysFromToday === 0) return null 67 | 68 | // otherwise, date is in next 7 days - include day of week 69 | const dayString = schedulingLib.getDayOfWeek(date) 70 | return `${dayString} (${dateString})` 71 | } 72 | 73 | schedulingLib.isAfterToday = (date) => { 74 | // get start of tomorrow 75 | const daysToAdd = new DateComponents() 76 | daysToAdd.day = 1 77 | const startOfTomorrow = Calendar.current.startOfDay(Calendar.current.dateByAddingDateComponents(new Date(), daysToAdd)) 78 | 79 | return date >= startOfTomorrow 80 | } 81 | 82 | schedulingLib.schedulingTag = () => { 83 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 84 | const schedulingTagID = syncedPrefs.read('schedulingTagID') 85 | if (schedulingTagID === null) return null 86 | else return Tag.byIdentifier(schedulingTagID) 87 | } 88 | 89 | schedulingLib.getSchedulingTag = async () => { 90 | const schedulingTag = schedulingLib.schedulingTag() 91 | if (schedulingTag !== null) return schedulingTag 92 | 93 | // not set - show preferences pane and then try again) 94 | await this.action('preferences').perform() 95 | return await schedulingLib.getSchedulingTag() 96 | } 97 | 98 | schedulingLib.createTag = async date => { 99 | const parent = await schedulingLib.getSchedulingTag() 100 | const tag = new Tag(schedulingLib.getString(date), parent) 101 | await schedulingLib.recreateTagOrder() 102 | return tag 103 | } 104 | 105 | schedulingLib.getTag = async (date) => { 106 | const dateString = schedulingLib.getDateString(date) 107 | if (dateString === null) return schedulingLib.todayTag() 108 | 109 | const parent = await schedulingLib.getSchedulingTag() 110 | const tag = parent.children.find(tag => tag.name === dateString || tag.name.includes(`(${dateString})`)) || await schedulingLib.createTag(date) 111 | return tag 112 | } 113 | 114 | schedulingLib.getDate = (tag) => { 115 | const formatter = schedulingLib.getDateFormatter() 116 | const dateString = schedulingLib.getDateStringFromTag(tag) 117 | const date = formatter.dateFromString(dateString) 118 | return date 119 | } 120 | 121 | schedulingLib.getDateStringFromTag = (tag) => { 122 | const matches = tag.name.match(/ \((.*)\)$/) 123 | return matches ? matches[1] : tag.name 124 | } 125 | 126 | schedulingLib.isToday = (date) => Calendar.current.startOfDay(date).getTime() === Calendar.current.startOfDay(new Date()).getTime() 127 | 128 | schedulingLib.promptAndReschedule = async (tasks) => { 129 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 130 | const useScheduledNotifications = syncedPrefs.readBoolean('useScheduledNotifications') 131 | 132 | const form = new Form() 133 | 134 | form.addField(new Form.Field.Date('date', 'Date', null, schedulingLib.getDateFormatter())) 135 | form.validate = (form) => form.values.date && (schedulingLib.isAfterToday(form.values.date) || schedulingLib.isToday(form.values.date)) 136 | 137 | await form.show('(Re)schedule to...', '(Re)schedule') 138 | 139 | for (const task of tasks) await schedulingLib.rescheduleTask(task, form.values.date) 140 | } 141 | 142 | schedulingLib.rescheduleTask = async (task, date) => { 143 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 144 | const schedulingTag = await schedulingLib.getSchedulingTag() 145 | const todayTag = schedulingLib.todayTag() 146 | const schedulingTags = schedulingTag.children 147 | const useScheduledNotifications = syncedPrefs.readBoolean('useScheduledNotifications') 148 | 149 | if (schedulingLib.isToday(date)) { 150 | // flag/tag as appropriate 151 | schedulingLib.addToToday(task) 152 | // remove old tags 153 | task.removeTags(schedulingTags) 154 | } else { 155 | // unflag task 156 | if (syncedPrefs.readBoolean('flagToday')) task.flagged = false 157 | 158 | // remove old tags 159 | task.removeTags(schedulingTags) 160 | if (todayTag !== null) task.removeTag(todayTag) 161 | 162 | // add new tag 163 | const dateTag = await schedulingLib.getTag(date) 164 | task.addTag(dateTag) 165 | } 166 | 167 | if (useScheduledNotifications) { 168 | 169 | // remove old notifications 170 | for (notification of task.notifications) task.removeNotification(notification) 171 | // add new notification 172 | const defaultScheduledTime = settings.objectForKey('DefaultScheduledNotificationTime') 173 | const defaultScheduledTimeSplit = defaultScheduledTime.split(':') 174 | const defaultScheduledHours = defaultScheduledTimeSplit[0] 175 | const defaultScheduledMinutes = defaultScheduledTimeSplit[1] 176 | date.setHours(defaultScheduledHours,defaultScheduledMinutes,0) 177 | task.addNotification(date) 178 | } 179 | 180 | } 181 | 182 | schedulingLib.unscheduleTasks = async (tasks) => { 183 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 184 | const todayTag = schedulingLib.todayTag() 185 | const schedulingTags = schedulingLib.schedulingTag().children 186 | for (task of tasks) { 187 | if (syncedPrefs.readBoolean('flagToday')) task.flagged = false 188 | if (syncedPrefs.readBoolean('useScheduledNotifications')) { 189 | for (notification of task.notifications) task.removeNotification(notification) 190 | } 191 | if (todayTag !== null) task.removeTag(todayTag) 192 | task.removeTags(schedulingTags) 193 | } 194 | 195 | } 196 | 197 | schedulingLib.addToToday = (task) => { 198 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 199 | if (syncedPrefs.readBoolean('flagToday')) task.flagged = true 200 | 201 | const todayTag = schedulingLib.todayTag() 202 | if (todayTag !== null) task.addTag(todayTag) 203 | } 204 | 205 | schedulingLib.makeToday = (tag) => { 206 | for (const task of tag.tasks) schedulingLib.addToToday(task) 207 | deleteObject(tag) 208 | } 209 | 210 | schedulingLib.recreateTagOrder = async () => { 211 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 212 | const schedulingTag = await schedulingLib.getSchedulingTag() 213 | const schedulingTags = schedulingTag.children 214 | const orderedTags = [] 215 | 216 | // make sure 'Tomorrow' and remaining week tags exists and are named correctly 217 | for (let i = 1; i <= 7; i++) { 218 | const daysToAdd = new DateComponents() 219 | daysToAdd.day = i 220 | const date = Calendar.current.dateByAddingDateComponents(new Date(), daysToAdd) 221 | 222 | // add/rename date-specific tag 223 | const dayTag = await schedulingLib.getTag(date) || await schedulingLib.createTag(date) 224 | dayTag.name = schedulingLib.getString(date) 225 | orderedTags.push(dayTag) 226 | 227 | // add/rename weekday tag if using weekdays 228 | if (syncedPrefs.readBoolean('useWeekdays')) { 229 | const weekdayTag = await schedulingLib.getWeekdayTag(date) 230 | orderedTags.push(weekdayTag) 231 | } 232 | } 233 | 234 | const futureTags = schedulingTags.filter(tag => !orderedTags.includes(tag)) 235 | const sortedFutureTags = futureTags.sort((a, b) => schedulingLib.getDate(a) - schedulingLib.getDate(b)) 236 | const sorted = orderedTags.concat(sortedFutureTags) 237 | 238 | moveTags(sorted, schedulingTag) 239 | } 240 | 241 | schedulingLib.updateTags = async () => { 242 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 243 | const lastUpdated = syncedPrefs.readDate('lastUpdated') 244 | 245 | const schedulingTag = await schedulingLib.getSchedulingTag() 246 | const schedulingTags = schedulingTag.children 247 | 248 | for (const tag of schedulingTags) { 249 | const date = schedulingLib.getDate(tag) 250 | 251 | // move any tags from the past into 'Today' 252 | if (date !== null && date <= new Date()) schedulingLib.makeToday(tag) 253 | 254 | // remove future date tags with no remaining tasks 255 | else if (date !== null && schedulingLib.daysFromToday(date) > 7 && tag.remainingTasks.length === 0) deleteObject(tag) 256 | } 257 | 258 | // weekdays - make current days current, note in synced prefs when last updated - if using weekdays 259 | if (syncedPrefs.readBoolean('useWeekdays') && (lastUpdated === null || !schedulingLib.isToday(lastUpdated))) { 260 | const weekdayTag = await schedulingLib.getWeekdayTag(new Date()) 261 | for (const task of weekdayTag.tasks) if (!schedulingLib.isAfterToday(task.effectiveDeferDate)) schedulingLib.addToToday(task) 262 | } 263 | 264 | await schedulingLib.recreateTagOrder() 265 | 266 | syncedPrefs.write('lastUpdated', new Date()) 267 | } 268 | 269 | schedulingLib.getScheduleInfo = async (task) => { 270 | const syncedPrefs = schedulingLib.loadSyncedPrefs() 271 | 272 | const schedulingTag = await schedulingLib.getSchedulingTag() 273 | const schedulingTags = schedulingTag.children 274 | 275 | const appliedTags = task.tags.filter(tag => schedulingTags.includes(tag)) 276 | 277 | const oxford = (arr, conjunction, ifempty) => { 278 | let l = arr.length; 279 | if (!l) return ifempty; 280 | if (l<2) return arr[0]; 281 | if (l<3) return arr.join(` ${conjunction} `); 282 | arr = arr.slice(); 283 | arr[l-1] = `${conjunction} ${arr[l-1]}`; 284 | return arr.join(", "); 285 | } 286 | 287 | return oxford(appliedTags.map(t => t.name), 'and', '') 288 | } 289 | 290 | return schedulingLib 291 | })() 292 | -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/unscheduleTask.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Form */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | await this.schedulingLib.unscheduleTasks([...selection.tasks, ...selection.projects.map(p => p.task)]) 5 | }) 6 | 7 | action.validate = function (selection, sender) { 8 | return [...selection.tasks, ...selection.projects].length > 0 9 | } 10 | 11 | return action 12 | })() 13 | -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/Resources/updateSchedule.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | await this.schedulingLib.updateTags() 5 | }) 6 | 7 | action.validate = function (selection, sender) { 8 | return true 9 | } 10 | 11 | return action 12 | })() 13 | -------------------------------------------------------------------------------- /Scheduling.omnifocusjs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultLocale": "en", 3 | "identifier": "com.KaitlinSalzke.Scheduling", 4 | "author": "Kaitlin Salzke", 5 | "description": "A series of scripts to assist with scheduling in OmniFocus.", 6 | "version": "2.5.0", 7 | "actions": [ 8 | { 9 | "identifier": "rescheduleTask", 10 | "image": "calendar.badge.plus" 11 | }, 12 | { 13 | "identifier": "unscheduleTask", 14 | "image": "calendar.badge.minus" 15 | }, 16 | { 17 | "identifier": "updateSchedule", 18 | "image": "sun.max" 19 | }, 20 | { 21 | "identifier": "preferences", 22 | "image": "gear" 23 | } 24 | ], 25 | "libraries": [ 26 | { 27 | "identifier": "schedulingLib" 28 | } 29 | ] 30 | } --------------------------------------------------------------------------------