Skip to content

Instantly share code, notes, and snippets.

@drewkerr
Last active July 25, 2024 09:36
Show Gist options
  • Save drewkerr/0f2b61ce34e2b9e3ce0ec6a92ab05c18 to your computer and use it in GitHub Desktop.
Save drewkerr/0f2b61ce34e2b9e3ce0ec6a92ab05c18 to your computer and use it in GitHub Desktop.
Read the current Focus mode on macOS Monterey (12.0+) using JavaScript for Automation (JXA)
const app = Application.currentApplication()
app.includeStandardAdditions = true
function getJSON(path) {
const fullPath = path.replace(/^~/, app.pathTo('home folder'))
const contents = app.read(fullPath)
return JSON.parse(contents)
}
function run() {
let focus = "No focus" // default
const assert = getJSON("~/Library/DoNotDisturb/DB/Assertions.json").data[0].storeAssertionRecords
const config = getJSON("~/Library/DoNotDisturb/DB/ModeConfigurations.json").data[0].modeConfigurations
if (assert) { // focus set manually
const modeid = assert[0].assertionDetails.assertionDetailsModeIdentifier
focus = config[modeid].mode.name
} else { // focus set by trigger
const date = new Date
const now = date.getHours() * 60 + date.getMinutes()
for (const modeid in config) {
const triggers = config[modeid].triggers.triggers[0]
if (triggers && triggers.enabledSetting == 2) {
const start = triggers.timePeriodStartTimeHour * 60 + triggers.timePeriodStartTimeMinute
const end = triggers.timePeriodEndTimeHour * 60 + triggers.timePeriodEndTimeMinute
if (start < end) {
if (now >= start && now < end) {
focus = config[modeid].mode.name
}
} else if (start > end) { // includes midnight
if (now >= start || now < end) {
focus = config[modeid].mode.name
}
}
}
}
}
return focus
}
@drewkerr
Copy link
Author

drewkerr commented Nov 3, 2022

@devnoname120 I'm not sure if there's an easy programmatic way. See the Automators discussion link above.

@Moonmonkey-Beep
Copy link

Moonmonkey-Beep commented Dec 26, 2022

Thanks, can you clarify what this is doing? Its pulling the contents of Assertions.json and ModeConfigurations.json
What is it doing with these? I assume some is pulling a setting from assertions and then looking it up in Modeconfigurations to get the focus name, but what is it looking up? I tried looking at the files and can't work it out. Thanks so much.

@drewkerr
Copy link
Author

Sure! ModeConfigurations.json contains the modes. Assertions.json contains the current mode, if set manually. If not, we loop over the mode configurations to see if any of the configured trigger times - which is most of the code - apply. If nothing applies, return "No focus".

@Moonmonkey-Beep
Copy link

Thanks so much for that. I'm still confused about what exactly is being cross matched from assertions in ModeConfigurations, For manually set focus I assumed that it was matching one of the identifiers i.e. 56361E65-B77B-4467-9C31-433E51BF0CCC, but none of them in assertions seem to match the ones in ModeConfigurations, I'm a bit baffled!

@drewkerr
Copy link
Author

It will match things like com.apple.focus.personal-time in both files. Look at lines 13-14 to get an idea of the structure of those files. It doesn't seem to work to edit those files, if that's what you're hoping to do. It's possible to set the focus by scripting the UI though.

@Moonmonkey-Beep
Copy link

Thanks so much! no, I am trying to write a swift version to pull the focus mode, but I have a feeling the Apple Sandboxing won't let me access the user library.

Thanks again for making this and also explaining it, much appreciated.

@jaimefordham
Copy link

jaimefordham commented Jan 19, 2023

Doesn't seem to return active focus mode name if its been set from another device (in my case an iPhone).

MacOS does recognise its in a Focus mode on the menubar but the script returns:

❯ osascript -l JavaScript ~/scripts/get-focus-mode.js
No focus

Contents of ~/Library/DoNotDisturb/DB/ModeConfigurations.json for active Work mode are:

"com.apple.focus.work": { "triggers": { "triggers": [] }, "automaticallyGenerated": false, "mode": { "name": "Work", "tintColorName": "systemTealColor", "identifier": "<REMOVED>", "semanticType": 4, "symbolImageName": "person.lanyardcard.fill", "modeIdentifier": "com.apple.focus.work", "visibility": 0 }, "dimsLockScreen": 0, "configuration": { "suppressionType": 2, "compatibilityVersion": 3, "configurationType": 0, "minimumBreakthroughUrgency": 1, "hideApplicationBadges": 1 }, "created": 1671056911.476048, "compatibilityVersion": 2, "hasSecureData": true, "impactsAvailability": 0, "lastModified": 1674036685.879897 },

Which suggests that its not matching the following condition:

if (triggers && triggers.enabledSetting == 2)

Maybe a focus mode triggered via geo-location doesn't get written out to ~/Library/DoNotDisturb/DB/ModeConfigurations.json ?

@Coder84619
Copy link

I'm running the script on Ventura 13.4.1, and I get:[ ](focus.js: execution error: Error: Error: Can't convert types. (-1700))

@drewkerr
Copy link
Author

drewkerr commented Jul 4, 2023

Not sure. It's working for me on the same version, running in Script Editor and Shortcuts, as long as Full Disk Access is allowed in System Settings (siriactionsd, in the case of Shortcuts). This isn't meant to be much more than a hack though.

@devnoname120
Copy link

devnoname120 commented Jul 5, 2023

I use the following Alfred Workflow in order to enable/disable DND: https://github.com/vitorgalvao/calm-notifications-workflow/tree/main/Workflow

It uses its own Shortcut under the hood that needs to be installed first.

I post it here as an example of a working implementation that people can look into.

@roman-ld
Copy link

Python if you want in that context.

#!/usr/bin/env python3

import json
import os
import datetime

ASSERT_PATH = os.path.expanduser("~/Library/DoNotDisturb/DB/Assertions.json")
MODECONFIG_PATH = os.path.expanduser("~/Library/DoNotDisturb/DB/ModeConfigurations.json")
def get_focus():
    focus = "No focus" #default
    assertJ = json.load(open(ASSERT_PATH))['data'][0]['storeAssertionRecords']
    configJ = json.load(open(MODECONFIG_PATH))['data'][0]['modeConfigurations']
    if assertJ:
        modeid = assertJ[0]['assertionDetails']['assertionDetailsModeIdentifier']
        focus = configJ[modeid]['mode']['name']
    else:
        date = datetime.datetime.today()
        now = date.hour * 60 + date.minute

        for modeid in configJ:
            triggers = configJ[modeid]['triggers']['triggers'][0]
            if triggers and triggers['enabledSetting'] == 2:
                start = triggers['timePeriodStartTimeHour'] * 60 + triggers['timePeriodStartTimeMinute']
                end = triggers['timePeriodEndTimeHour'] * 60 + triggers['timePeriodEndTimeMinute']
                if start < end:
                    if now >= start and now < end:
                        focus = configJ[modeid]['mode']['name']
                elif start > end: # includes midnight
                    if now >= start or now < end:
                        focus = configJ[modeid]['mode']['name']
    return focus

if '__main__' == __name__:
    print(get_focus())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment