From 75eea024541ef5b6053cc2f31ece3c1bf169e29a Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Mon, 5 Oct 2026 09:51:09 -0400 Subject: [PATCH] feat(mac): launch config instead of restart --- mac/Config/Config/ConfigApp.swift | 20 ++++-- .../Installation/InstallationCheck.swift | 65 +++++++++++++++++++ .../Installation/InstallationContainer.swift | 26 +++++++- mac/Config/Localizable.xcstrings | 3 + mac/installer/build.sh | 2 +- .../en.lproj/conclusion.rtf | 6 +- mac/installer/scripts/postinstall | 27 +++++++- 7 files changed, 140 insertions(+), 9 deletions(-) diff --git a/mac/Config/Config/ConfigApp.swift b/mac/Config/Config/ConfigApp.swift index 1e902c9a0c..18e4920d5e 100644 --- a/mac/Config/Config/ConfigApp.swift +++ b/mac/Config/Config/ConfigApp.swift @@ -34,6 +34,18 @@ struct ConfigApp: App { options.releaseName = ConfigAppUtil.versionGitTag options.environment = ConfigAppUtil.sentryEnvironment } + + // get array of arguments passed to binary + let arguments = ProcessInfo.processInfo.arguments + + if arguments.contains("--installation") { + let installationContainer = _installation.wrappedValue + + // This Task fires on the next run-loop tick after StateObject is allocated + Task { @MainActor in + installationContainer.executeNewInstall() + } + } } var body: some Scene { @@ -73,10 +85,10 @@ struct ConfigApp: App { } // for testing purposes -// Window("Install Test", id: "install-debug") { -// InstallDebugView() -// .environmentObject(installation) -// } + Window("Install Test", id: "install-debug") { + InstallDebugView() + .environmentObject(installation) + } } } diff --git a/mac/Config/Installation/InstallationCheck.swift b/mac/Config/Installation/InstallationCheck.swift index a1490153b9..1d0829255a 100644 --- a/mac/Config/Installation/InstallationCheck.swift +++ b/mac/Config/Installation/InstallationCheck.swift @@ -15,6 +15,7 @@ import Foundation import KeymanSettings import OSLog +import Carbon // The data model version number is updated by the input method. // We only read the value here, and if it is current, then there is no need @@ -250,6 +251,15 @@ public class InstallationCheck { name: NSNotification.Name.accessibilityStateResponse, object: nil // Observe notifications from any sender ) + + let notificationName = Notification.Name(kTISNotifyEnabledKeyboardInputSourcesChanged as String) + DistributedNotificationCenter.default().addObserver( + self, + selector: #selector(self.handleInputSourcesChanged(_:)), + name: notificationName, + object: nil // Observe notifications from any sender + ) + // TODO: add timeout in case response is not received // see issue #16579 } @@ -289,6 +299,61 @@ public class InstallationCheck { Logger.app.debug("handleAccessibilityResponse, received but did not include message") } } + + /** + * called when `kTISNotifyEnabledKeyboardInputSourcesChanged` is received + */ + @objc func handleInputSourcesChanged(_ notification: Notification) { + Logger.app.debug("handleKeyboardEnabled received message: \(notification, privacy: .public)") + let inputSourceState = self.checkInputSources() + } + + public func checkInputSources() -> (registered: Bool, enabled: Bool) { + // Correct Carbon filtering keys +// let properties = [ +// kTISPropertyInputSourceCategory: kTISCategoryKeyboardInputSource +// ] as CFDictionary + var inputSourceState: (registered: Bool, enabled: Bool) = (false, false) + + let inputSourceId = InputMethodUtil.keymanBundleId + let properties = [ + kTISPropertyInputSourceID: inputSourceId as CFString + ] as CFDictionary + + Logger.app.debug("🚦checking input sources") + + // use includesAllInstalled = true, so we get unregistered input methods + guard let listRef = TISCreateInputSourceList(properties, true) else { + Logger.app.error("Unable to fetch input source list.") + return inputSourceState + } + + let inputSourceList = listRef.takeRetainedValue() as! [TISInputSource] + + if (!inputSourceList.isEmpty) { + guard let source = inputSourceList.first else { + return inputSourceState + } + + guard let idPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), + let enabledPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) else { + return inputSourceState + } + + inputSourceState.registered = true + + // Extract the string Identifier and boolean Enablement state + let sourceID = Unmanaged.fromOpaque(idPtr).takeUnretainedValue() as String + let isEnabled = Unmanaged.fromOpaque(enabledPtr).takeUnretainedValue() as! Bool + + inputSourceState.enabled = isEnabled + Logger.app.debug("Source ID: \(sourceID, privacy: .public) | Is Enabled: \(isEnabled)") + } else { + Logger.app.debug("Keyman input source not registered in system.") + } + + return inputSourceState + } /** * Process the distributed notification message that we received from the Keyman input method. diff --git a/mac/Config/Installation/InstallationContainer.swift b/mac/Config/Installation/InstallationContainer.swift index ffe6123a10..6a00a083dd 100644 --- a/mac/Config/Installation/InstallationContainer.swift +++ b/mac/Config/Installation/InstallationContainer.swift @@ -67,10 +67,34 @@ public class InstallationContainer : ObservableObject { self.confirmUserRestarted() } else { self.registerObservers() - self.installationCheck.startInstallationEvaluation() + + // use command line argument to begin installation instead + //self.installationCheck.startInstallationEvaluation() } } + /** + * called immediately after startup when invoked from installer post-install script + */ + func executeNewInstall() { + Logger.app.debug("✅ executing new installation...") + var didRegister = false + var didEnable = false + + // test to see if input method is registered or active + let inputSourceState = self.installationCheck.checkInputSources() + + if !inputSourceState.registered { + didRegister = self.inputMethodUtil.registerKeymanInputMethod() + } + + if !inputSourceState.enabled { + didEnable = self.inputMethodUtil.enableKeymanInputMethod() + } + + Logger.app.debug("new installation, didRegister: \(didRegister), didEnable: \(didEnable)") + } + /** * register observers to learn of results of InstallationState evaluation */ diff --git a/mac/Config/Localizable.xcstrings b/mac/Config/Localizable.xcstrings index 2c2735ae2e..900b2f662c 100644 --- a/mac/Config/Localizable.xcstrings +++ b/mac/Config/Localizable.xcstrings @@ -77,6 +77,9 @@ }, "Install" : { + }, + "Install Test" : { + }, "Installation" : { diff --git a/mac/installer/build.sh b/mac/installer/build.sh index f9c7862627..7535041b25 100755 --- a/mac/installer/build.sh +++ b/mac/installer/build.sh @@ -148,7 +148,7 @@ cat < "${OUTPUT__DISTRIBUTION_XML}" - keyman-input-method.pkg + keyman-input-method.pkg EOF diff --git a/mac/installer/installer-resources/en.lproj/conclusion.rtf b/mac/installer/installer-resources/en.lproj/conclusion.rtf index 1344b408ab..cb4195271c 100644 --- a/mac/installer/installer-resources/en.lproj/conclusion.rtf +++ b/mac/installer/installer-resources/en.lproj/conclusion.rtf @@ -7,7 +7,9 @@ \pard\pardeftab720\partightenfactor0 \f0\fs26 \cf0 \expnd0\expndtw0\kerning0 -Restart your Mac, then run +Press the Close button.\ +\ + \f1\b Keyman Configuration -\f0\b0 located in the Applications folder to complete setup of Keyman.\ +\f0\b0 will launch to complete the setup of Keyman.\ } \ No newline at end of file diff --git a/mac/installer/scripts/postinstall b/mac/installer/scripts/postinstall index c9f45e2035..0a2146f0cb 100755 --- a/mac/installer/scripts/postinstall +++ b/mac/installer/scripts/postinstall @@ -50,7 +50,32 @@ echo "The installer package name is: $PACKAGE_PATH" # define path of tmp file to write keyboard info KEYBOARD_FILE_PATH="$3/var/tmp/keyboard-to-install.txt" -#write keyboard file +# write keyboard file /bin/echo "sil_euro_latin" > "${KEYBOARD_FILE_PATH}" +# get the Process ID of the parent installer framework that called this script +PARENT_PID=$PPID + +# run background process that launches Keyman Configuration app +( + # # wait up to 15 seconds for the installer processes to completely exit + # for i in {1..15}; do + # # ps -p checks if the specific parent installer process is still alive. + # # If it's dead, ps returns an error code, meaning the installer closed! + # if ! ps -p "$PARENT_PID" > /dev/null; then + # break + # fi + # sleep 1 + # done + + # give the file system a second to commit changes + sleep 1 + + # check if there is a current user and we are not sitting at the login window, then + # launch the Keyman Configuration under the current user with the custom install argument + if [ -n "$CONSOLE_USER" ] && [ "$CONSOLE_USER" != "loginwindow" ]; then + sudo -u "$CONSOLE_USER" open "/Applications/Keyman Configuration.app" --args --installation + fi +) & + exit 0