feat(mac): launch config instead of restart

This commit is contained in:
Shawn Schantz 2026-10-05 09:51:09 -04:00
parent 5e2ad19977
commit 75eea02454
7 changed files with 140 additions and 9 deletions

View file

@ -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)
}
}
}

View file

@ -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<CFString>.fromOpaque(idPtr).takeUnretainedValue() as String
let isEnabled = Unmanaged<CFBoolean>.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.

View file

@ -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
*/

View file

@ -77,6 +77,9 @@
},
"Install" : {
},
"Install Test" : {
},
"Installation" : {

View file

@ -148,7 +148,7 @@ cat <<EOF > "${OUTPUT__DISTRIBUTION_XML}"
<choice id="com.keyman.im.installer" visible="false">
<pkg-ref id="com.keyman.im.installer"/>
</choice>
<pkg-ref id="com.keyman.im.installer" version="$KEYMAN_VERSION" onConclusion="RequireRestart">keyman-input-method.pkg</pkg-ref>
<pkg-ref id="com.keyman.im.installer" version="$KEYMAN_VERSION" onConclusion="none">keyman-input-method.pkg</pkg-ref>
</installer-gui-script>
EOF

View file

@ -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.\
}

View file

@ -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