-
Notifications
You must be signed in to change notification settings - Fork 129
Add AppleScript Support #867
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add AppleScript Support #867
Conversation
|
Thank you for the contribution, Stephen! Regarding the plist change, it looks strange to me too. Can you share the actual changes needed here? Is it just: <key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>MarkEdit.sdef</string> |
MarkEdit.xcodeproj/project.pbxproj
Outdated
| INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||
| INFOPLIST_KEY_NSMainStoryboardFile = Main; | ||
| INFOPLIST_KEY_NSPrincipalClass = "$(PRODUCT_NAME).Application"; | ||
| INFOPLIST_KEY_NSPrincipalClass = MarkEdit.Application; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is using static name here required?
| get { | ||
| return self.stringValue | ||
| } | ||
| set(newValue) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: (newValue) can be omitted.
| import Carbon | ||
| import MarkEditKit | ||
|
|
||
| extension EditorDocument { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we name source and formattedText to be more scripting specific? Since we are creating extension that can be used in the entire app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If these are something to be used in the script, it's fine to keep them clean (or follow some conventions).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, would prefixing them with scripting be enough? E.g. scriptingSource?
| /// Executes a user-provided JS script in the document's webview. | ||
| @objc func handleEvaluateCommand(_ command: NSScriptCommand) -> Any? { | ||
| guard let inputString = command.evaluatedArguments?["script"] as? String else { | ||
| let scriptError = ScriptError.missingInput(message: "No JavaScript script provided") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this user-facing string? I can localize it later if so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would get presented to users as an AppleScript error, but I think it would only occur if the incoming event data gets corrupted, which is very rare. Just omitting the script parameter or supplying a non-text value gets handled by AppleScript before sending any messages to the application.
Maybe it's not worth keeping the message? The error number would still appear anyway.
| return nil | ||
| } | ||
|
|
||
| guard let targetEditor = self.windowControllers.first?.contentViewController as? EditorViewController else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: code convention wise, I prefer to hide self. if we can. I can do a cleanup later.
| static let RGBColorCoefficient: CGFloat = 65535 | ||
|
|
||
| /// Unpacks incoming color descriptors into NSColor objects. | ||
| @objc func scriptRGBColor(with descriptor: NSAppleEventDescriptor) -> NSColor? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious, are these functions called by scripts? Don't see a caller in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The get called automatically by the Apple Event Manager when users send/receive color objects via AppleScript. They're normally automatically generated at runtime, but that's skipped for colors due to how the text suite defines them.
| } | ||
|
|
||
| return NSColor( | ||
| calibratedRed: CGFloat(rgbColor.red) / Self.RGBColorCoefficient, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not an expert of this, just making sure we are using correct initializer, is calibrated needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, the only clear reference for those methods is iterm2. I think the unpack method can use any NSColor init method, but I'd need to check.
| // Created by Stephen Kaplan on 4/2/25. | ||
| // | ||
|
|
||
| import Carbon |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is deprecated, does Foundation work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, keyASUserRecordFields is only defined under Carbon, but without it, I don't know another way to construct valid NSAppleEventDescriptors for dictionaries with unknown keys, which is needed for handling results from JavaScript evaluation. Apple still uses it in non-archived documentation.
I can use the raw OSType int value instead, which I guess it better.
| case .noActiveEditor: | ||
| return -3 | ||
| case .jsEvalError: | ||
| return -1702 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why -1702?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, those values were placeholders. Was going to use the OSType of the AS event class instead.
|
Thanks again, Stephen. The change overall LGTM, I just left some comments. Regarding this:
I couldn't think of a better way either. I used similar delays in the app to handle a similar case (see I have one more general question about the change, since MarkEdit only handles plain text, why is rich text support needed? |
No problem, it was a fun challenge to tackle!
I believe so, but I'll have to make sure. Apple Events are very picky. Won't get the changes done until tomorrow, but I'll respond to your other comments now.
Hm, I was thinking about looking into
The text suite (which includes the 'rich text' AppleScript class) is the recommended way to access individual words, characters, etc. from AppleScript since repeating over elements is much, much faster in Swift/ObjC, so that's the reason for the additional property in general. Then it just seemed reasonable to use Swift's markdown support in NSAttributedString so you can detect when text is italic/bold without including the surrounding symbols in the "words" subproperty. |
|
I would like to update the wiki after we merge the change and looking for these example:
Could you please kindly provide some example scripts? Thanks! |
|
For the plist change, did you use Xcode to generate the change, or did you modify the plist file directly? Xcode may re-generate the entire file which may lead to unwanted changes. |
|
Made those changes, and added the error message strings to AppResources.Localized.Scripting.
Knew I was forgetting something. Added the Made some examples here: https://gist.github.com/SKaplanOfficial/9637cd69e2b21b1749521aafa1f71c95
I used Xcode, so that explains it! I've reverted those changes and manually added the ones for AppleScript. |
|
|
||
| <class name="document" code="docu" description="A MarkEdit document." plural="documents" inherits="document"> | ||
| <cocoa class="MarkEdit.EditorDocument"/> | ||
| <property name="source" code="mdsc" type="text" access="rw" description="The raw markdown source of the document."> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean the variable name in scripts is source, which uses scriptingSource in code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly.
| - Fast: edits 10 MB files easily | ||
| - Lightweight: installer size is about 3 MB | ||
| - Extensible: seamless Shortcuts integration | ||
| - Extensible: seamless integration with Shortcuts and AppleScript |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
I just tested and it works really well, going to check this in and do some follow-up changes like localization and cleanup, if you don't mind. |
|
Sounds good. Let me know if you need anything from me. |
| } | ||
|
|
||
| // Preserve newlines for better error reporting | ||
| let jsText = inputString.replacingOccurrences(of: "\\n", with: "\\\\n") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SKaplanOfficial this quite interesting to me, does this really work?
If we are replacing \n to \\n in the scripts, should it be inputString.replacingOccurrences(of: "\n", with: "\\n")?
I don't quite get why we need double backslashes, and why we need to replace them at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we can remove it. You're right, it doesn't look like it'd work anyway.
When I was testing earlier, the evaluate JavaScript command would report errors as if there were only one line (e.g. line 1, column n where n was the length of the entire string up to that point).
Something fixed that problem, but I don't think it was this.
I've added AppleScript equivalents for all of the Shortcuts, plus support for the AppleScript standard dictionary and text suite. The full supported terminology can be viewed via Script Editor's File->Open Dictionary menu or in
MarkEditMac/Resources/MarkEdit.sdefI haven't done much Swift development, so I probably used odd approaches in some places. Let me know if I need to rework anything. Also, I'm not sure why Info.plist got all rearranged — is there something I can do to fix that?
Here's a test script demonstrating what's currently supported:
Known issues:
sourceproperty when using themakecommand raises an error. This is because settingsourcerelies on there being an existing webview to callreplaceTexton. Current workaround: Use a momentary delay aftermake, then useset source to ....colorproperty of rich text elements is always{0, 0, 0}. This is expected since there is no color information included in the document'ssource. A future improvement could be to process the markdown source and add colors based on the current theme.Closes #854.