-
-
Save drewkerr/0f2b61ce34e2b9e3ce0ec6a92ab05c18 to your computer and use it in GitHub Desktop.
| 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 | |
| } |
You could avoid using the Objective-C bridge in your getJSON function by doing something like this:
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)
}Thanks @diogocampos! I was working from the JXA Cookbook for that. I've updated the gist.
I'm wondering if this could be done in shell if I had access to jq for parsing json. Unfortunately, I don't really know how to read this code as I don't real AppleJavaScript.
EDITED TO ADD: I remembered that I could do a HEREDOC with this in it…
CURRENT=$(/usr/bin/osascript -l JavaScript <<EOT
....
EOT
)
and the variable $CURRENT would have the result of the JavaScript. That will work for me, unless there's a pure bash/zsh/shell option out there that someone can come up with.
Awesome, thank you.
I'm trying to integrate focus mode into an Übersicht* widget, which uses JSX. My JS chops aren't good enough to figure how to port the functions though. Any tips? 🙏
@drewkerr I suggest you to add this to the top of the script so that it can be directly executed:
#!/usr/bin/env osascript -l JavaScript
@drewkerr Also — I would I go to (enable | disable) (DND | focus-mode) on macOS Monterey (12.6)?
@devnoname120 I'm not sure if there's an easy programmatic way. See the Automators discussion link above.
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.
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".
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!
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.
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.
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 ?
I'm running the script on Ventura 13.4.1, and I get:[ ](focus.js: execution error: Error: Error: Can't convert types. (-1700))
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.
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.
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())
@roman-ld Had this script running on Sequioa. Upgraded last weekend to Tahoe. Script is now broken, info is not anymore in Assertions. Would appreciate a fix.
@tooh welcome to macos updates!
Still working for me after upgrading to Tahoe.
I think I've found the problem, have you agreed to the "Xcode and Apple SDKs license" after upgrading?
I looked at what my python3 I was using (which python3).
Found I'm on homebrew for python3 (meaning I didn't have to agree for the python to work).
Used find to find other python3 on my path (find "${(s/:/)PATH}" -name python3).
Aside: Apple's zsh now has several entries that are not readable / search able from a non-admin account (don't worry about these if you run into this as a non-admin user).
find: /usr/sbin/authserver: Permission denied
find: /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin: No such file or directory
find: /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin: No such file or directory
find: /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin: No such file or directory
find: /Library/Apple/usr/bin: No such file or directory
When I switched the #! to #!/usr/bin/python3 (macos stock python3) I got:
You have not agreed to the Xcode and Apple SDKs license.
...
Bottom of that says:
Agreeing to the Xcode and Apple SDKs license requires admin privileges, please accept the Xcode license as the root user (e.g. 'sudo xcodebuild -license').
After agreeing (running the -license, typing accept) the script worked with macos stock python3
I hope this helps.
I just needed something dead-simple: focus mode on or off. None of the above were working without errors for me on Tahoe, so I adapted the Python example from @roman-ld above (thanks) and stripped out some parts that I don't need.
I saved this as /usr/local/bin/dnd and chmod +x'd it. “Works for me” but YMMV.
With this you can do things like if dnd -q ; then ... ; fi in shell scripts.
#!/usr/bin/env python3
"""
Check DND status - tested only on macOS 26.0 Tahoe
exits with 0 if a focus mode is enabled, 1 otherwise
prints mode unless -q/--quiet is passed as arg
based on https://gist.github.com/drewkerr/0f2b61ce34e2b9e3ce0ec6a92ab05c18
"""
import json
import os
import sys
ASSERTIONS_PATH = os.path.expanduser("~/Library/DoNotDisturb/DB/Assertions.json")
CONFIG_PATH = os.path.expanduser("~/Library/DoNotDisturb/DB/ModeConfigurations.json")
QUIET = False
def msg(m: str, q: bool) -> None:
if not q:
print(m)
def get_focus(q: bool) -> int:
try:
ASSERTION_RECS = json.load(open(ASSERTIONS_PATH))['data'][0]['storeAssertionRecords']
FOCUS_CONFIGS = json.load(open(CONFIG_PATH))['data'][0]['modeConfigurations']
MODE = ASSERTION_RECS[0]['assertionDetails']['assertionDetailsModeIdentifier']
msg(FOCUS_CONFIGS[MODE]['mode']['name'], q)
return 0
except:
msg("No focus", q)
return 1
if '__main__' == __name__:
if len(sys.argv) > 1:
if sys.argv[1] in ['-q', '--quiet']:
QUIET = True
sys.exit(get_focus(QUIET))
Discussion on Automators forum and Six Colors.