Skip to content

Commit

Permalink
Merge pull request #117 from vincentneo/applescript-api
Browse files Browse the repository at this point in the history
Add both AppleScript and scripting menu features
  • Loading branch information
vincentneo authored Dec 8, 2023
2 parents 991f1e0 + 9937c33 commit c7eecab
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 3 deletions.
12 changes: 10 additions & 2 deletions Quality.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
1293436B28131591002E19A8 /* CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1293436A28131591002E19A8 /* CurrentUser.swift */; };
12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12AFF5C02811AD40001CC6ED /* AppDelegate.swift */; };
12F1AA572868639A006C1AD8 /* DeviceMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */; };
BF0C90C82B20BFD6002F99C9 /* LosslessSwitcher.sdef in Resources */ = {isa = PBXBuildFile; fileRef = BF0C90C72B20BFD6002F99C9 /* LosslessSwitcher.sdef */; };
BF0C90CA2B20C163002F99C9 /* ScriptableApplicationCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C90C92B20C163002F99C9 /* ScriptableApplicationCommand.swift */; };
BF7E0D09296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */; };
/* End PBXBuildFile section */

Expand All @@ -47,6 +49,8 @@
1293436A28131591002E19A8 /* CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUser.swift; sourceTree = "<group>"; };
12AFF5C02811AD40001CC6ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMenuItem.swift; sourceTree = "<group>"; };
BF0C90C72B20BFD6002F99C9 /* LosslessSwitcher.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = LosslessSwitcher.sdef; sourceTree = "<group>"; };
BF0C90C92B20C163002F99C9 /* ScriptableApplicationCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptableApplicationCommand.swift; sourceTree = "<group>"; };
BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioStreamBasicDescription+Equatable.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -110,6 +114,8 @@
127C972C281FCF000087313B /* AppVersion.swift */,
12F1AA562868639A006C1AD8 /* DeviceMenuItem.swift */,
BF7E0D08296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift */,
BF0C90C92B20C163002F99C9 /* ScriptableApplicationCommand.swift */,
BF0C90C72B20BFD6002F99C9 /* LosslessSwitcher.sdef */,
);
path = Quality;
sourceTree = "<group>";
Expand Down Expand Up @@ -191,6 +197,7 @@
buildActionMask = 2147483647;
files = (
1272AA9F280DBB4B00FD72BA /* Preview Assets.xcassets in Resources */,
BF0C90C82B20BFD6002F99C9 /* LosslessSwitcher.sdef in Resources */,
1272AA9C280DBB4B00FD72BA /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -203,6 +210,7 @@
buildActionMask = 2147483647;
files = (
12AFF5C12811AD40001CC6ED /* AppDelegate.swift in Sources */,
BF0C90CA2B20C163002F99C9 /* ScriptableApplicationCommand.swift in Sources */,
1254A79C2813FB9400241107 /* Defaults.swift in Sources */,
BF7E0D09296336DA009FFEEC /* AudioStreamBasicDescription+Equatable.swift in Sources */,
1234F50E281E8F07007EC9F5 /* MediaTrack.swift in Sources */,
Expand Down Expand Up @@ -342,7 +350,7 @@
CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\"";
DEVELOPMENT_TEAM = 3X69W4AQD6;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down Expand Up @@ -376,7 +384,7 @@
CODE_SIGN_ENTITLEMENTS = Quality/Quality.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_ASSET_PATHS = "\"Quality/Preview Content\"";
DEVELOPMENT_TEAM = 3X69W4AQD6;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down
39 changes: 38 additions & 1 deletion Quality/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {

// https://stackoverflow.com/a/66160164
static private(set) var instance: AppDelegate! = nil
private var outputDevices: OutputDevices!
var outputDevices: OutputDevices!
private let defaults = Defaults.shared
private var mrController: MediaRemoteController!
private var devicesMenu: NSMenu!

var statusItem: NSStatusItem?
var cancellable: AnyCancellable?

var currentScriptSelectionMenuItem: NSMenuItem?

private var _statusItemTitle = "Loading..."
var statusItemTitle: String {
Expand Down Expand Up @@ -62,6 +64,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
checkPermissions()

let menu = NSMenu()

menu.delegate = self

let sampleRateView = ContentView().environmentObject(outputDevices)
let view = NSHostingView(rootView: sampleRateView)
Expand Down Expand Up @@ -96,6 +100,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
aboutItem.submenu?.addItem(buildItem)
menu.addItem(aboutItem)

let scriptMenu = NSMenuItem(title: "Scripting", action: nil, keyEquivalent: "")
let selectScript = NSMenuItem(title: "Select Script...", action: #selector(selectScript(_:)), keyEquivalent: "")
let resetScript = NSMenuItem(title: "Clear selection", action: #selector(resetScript(_:)), keyEquivalent: "")
let currentScriptSelectionMenuItem = NSMenuItem(title: "No selection", action: nil, keyEquivalent: "")
self.currentScriptSelectionMenuItem = currentScriptSelectionMenuItem
scriptMenu.submenu = NSMenu()
scriptMenu.submenu?.addItem(selectScript)
scriptMenu.submenu?.addItem(resetScript)
scriptMenu.submenu?.addItem(currentScriptSelectionMenuItem)
menu.addItem(scriptMenu)

let quitItem = NSMenuItem(title: "Quit", action: #selector(NSApp.terminate(_:)), keyEquivalent: "")
menu.addItem(quitItem)

Expand Down Expand Up @@ -175,4 +190,26 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}

@objc func selectScript(_ item: NSMenuItem) {
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
panel.message = "Select a script that should be invoked when sample rate changes."

panel.begin { response in
Defaults.shared.shellScriptPath = panel.url?.path
}
}

@objc func resetScript(_ item: NSMenuItem) {
Defaults.shared.shellScriptPath = nil
}

}

extension AppDelegate: NSMenuDelegate {
func menuWillOpen(_ menu: NSMenu) {
currentScriptSelectionMenuItem?.title = Defaults.shared.shellScriptPath ?? "No selection"
}
}
10 changes: 10 additions & 0 deletions Quality/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Defaults: ObservableObject {
private let kUserPreferIconStatusBarItem = "com.vincent-neo.LosslessSwitcher-Key-UserPreferIconStatusBarItem"
private let kSelectedDeviceUID = "com.vincent-neo.LosslessSwitcher-Key-SelectedDeviceUID"
private let kUserPreferBitDepthDetection = "com.vincent-neo.LosslessSwitcher-Key-BitDepthDetection"
private let kShellScriptPath = "KeyShellScriptPath"

private init() {
UserDefaults.standard.register(defaults: [
Expand Down Expand Up @@ -40,6 +41,15 @@ class Defaults: ObservableObject {
}
}

var shellScriptPath: String? {
get {
return UserDefaults.standard.string(forKey: kShellScriptPath)
}
set {
UserDefaults.standard.setValue(newValue, forKey: kShellScriptPath)
}
}

@Published var userPreferBitDepthDetection: Bool


Expand Down
4 changes: 4 additions & 0 deletions Quality/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
<true/>
<key>NSAppleEventsUsageDescription</key>
<string>This permission is required for local file sample rate detection.</string>
<key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>LosslessSwitcher.sdef</string>
</dict>
</plist>
10 changes: 10 additions & 0 deletions Quality/LosslessSwitcher.sdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="LosslessSwitcher Terminology">
<suite name="LS Suite" code="sdse" description="Standard commands for this application">
<command name="get latest sample rate" code="getlrate" description="returns the last known sample rate">
<cocoa class="LosslessSwitcher.ScriptableApplicationCommand"/>
<result type="integer" description="sample rate"/>
</command>
</suite>
</dictionary>
19 changes: 19 additions & 0 deletions Quality/OutputDevices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,5 +252,24 @@ class OutputDevices: ObservableObject {
let delegate = AppDelegate.instance
delegate?.statusItemTitle = String(format: "%.1f kHz", readableSampleRate)
}
self.runUserScript(sampleRate)
}

func runUserScript(_ sampleRate: Float64) {
guard let scriptPath = Defaults.shared.shellScriptPath else { return }
let argumentSampleRate = String(Int(sampleRate))
Task.detached {
let scriptURL = URL(fileURLWithPath: scriptPath)
do {
let task = try NSUserUnixTask(url: scriptURL)
let arguments = [
argumentSampleRate
]
try await task.execute(withArguments: arguments)
}
catch {
print("TASK ERR \(error)")
}
}
}
}
22 changes: 22 additions & 0 deletions Quality/ScriptableApplicationCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// ScriptableApplicationCommand.swift
// LosslessSwitcher
//
// Created by Vincent Neo on 6/12/23.
//

import Cocoa

class ScriptableApplicationCommand: NSScriptCommand {

override func performDefaultImplementation() -> Any? {
guard let delegate = AppDelegate.instance else {
return -1000
}
let od = delegate.outputDevices
guard let sampleRate = od?.currentSampleRate else {
return -1
}
return Int(sampleRate * 1000)
}
}

0 comments on commit c7eecab

Please sign in to comment.