mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-14 11:37:43 +00:00
feat(mac): input method now receives correct version number
handle situation where input method has been moved or renamed before doing a version number check
This commit is contained in:
parent
ccd5f18fc6
commit
de4c736cfe
5 changed files with 87 additions and 64 deletions
|
|
@ -17,6 +17,11 @@ extension Notification.Name {
|
|||
static let accessCheck = Notification.Name("com.keyman.accessibility.state")
|
||||
}
|
||||
|
||||
public enum KeymanVersionCheckError: Error {
|
||||
case inputMethodNotFound
|
||||
case versionNotFound
|
||||
}
|
||||
|
||||
public enum KeymanInvocationError: Error {
|
||||
case inputMethodNotFound
|
||||
case inputMethodCouldNotBeInvoked
|
||||
|
|
@ -38,7 +43,19 @@ public class InputMethodUtil {
|
|||
public init() {
|
||||
self.pathUtil = KeymanPaths()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* true if the Keyman input method exists at `~/Library/Input Methods`
|
||||
*/
|
||||
public func keymanInputMethodExists() -> Bool {
|
||||
guard let inputMethodUrl = pathUtil.buildInputMethodPathUrl(fileName: self.keymanInputMethodApplicationName) else {
|
||||
print("Keyman input method not found, failed to create input method url")
|
||||
return false
|
||||
}
|
||||
|
||||
return FileManager.default.fileExists(atPath: inputMethodUrl.path)
|
||||
}
|
||||
|
||||
/**
|
||||
* true if the Keyman input method of the correct version exists in the correct location
|
||||
*/
|
||||
|
|
@ -50,8 +67,8 @@ public class InputMethodUtil {
|
|||
/**
|
||||
* Returns version number string of Keyman input method
|
||||
*/
|
||||
public func getKeymanInputMethodVersion() -> String? {
|
||||
return self.appVersion(applicationName: keymanInputMethodApplicationName)
|
||||
public func getKeymanInputMethodVersion() throws -> String {
|
||||
return try self.appVersionForInputMethod(applicationName: keymanInputMethodApplicationName)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -122,37 +139,26 @@ public class InputMethodUtil {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns version number string for application with specified name
|
||||
* Returns version number string for the specifed app located at `~/Library/Input Methods`
|
||||
*/
|
||||
func appVersion(applicationName: String) -> String? {
|
||||
var version: String? = nil
|
||||
|
||||
if let location = pathUtil.buildInputMethodPathUrl(fileName: applicationName) {
|
||||
print("app location: \(location.absoluteString)")
|
||||
if let keyputBundle = Bundle(url: location) {
|
||||
print("app bundle path: \(keyputBundle.bundlePath)")
|
||||
if let infoDictionary = keyputBundle.infoDictionary {
|
||||
print("infoDictionary key count: \(infoDictionary.count)")
|
||||
if let appVersion = infoDictionary["CFBundleShortVersionString"] as? String {
|
||||
print("app '\(applicationName)', appVersion: \(appVersion)")
|
||||
version = appVersion
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("cannot create KeyPut bundle")
|
||||
}
|
||||
/*
|
||||
if let keyputBundle = Bundle(url: location), let infoDictionary = keyputBundle.infoDictionary, let appVersion = infoDictionary["CFBundleShortVersionString"] as? String {
|
||||
ConfigLogger.shared.testLogger.debug("App Version: \(appVersion)")
|
||||
} else {
|
||||
ConfigLogger.shared.testLogger.debug("App Version not found")
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
print("app '\(applicationName)' not found")
|
||||
func appVersionForInputMethod(applicationName: String) throws -> String {
|
||||
guard let location = pathUtil.buildInputMethodPathUrl(fileName: applicationName) else {
|
||||
throw KeymanVersionCheckError.inputMethodNotFound
|
||||
}
|
||||
|
||||
return version
|
||||
guard let keymanBundle = Bundle(url: location) else {
|
||||
throw KeymanVersionCheckError.inputMethodNotFound
|
||||
}
|
||||
|
||||
guard let infoDictionary = keymanBundle.infoDictionary else {
|
||||
throw KeymanVersionCheckError.versionNotFound
|
||||
}
|
||||
|
||||
guard let appVersionString = infoDictionary["CFBundleShortVersionString"] as? String else {
|
||||
throw KeymanVersionCheckError.versionNotFound
|
||||
}
|
||||
|
||||
return appVersionString
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,19 +13,32 @@ import KeymanSettings
|
|||
|
||||
|
||||
public class InstallationCheck {
|
||||
fileprivate let keymanVersion: String
|
||||
fileprivate let inputMethodVersion: String
|
||||
fileprivate let defaultsRepository: DefaultsRepo
|
||||
fileprivate let inputMethodUtil: InputMethodUtil
|
||||
fileprivate let inputMethodExists: Bool
|
||||
|
||||
public init(version: String, defaultsRepo: DefaultsRepo, inputMethodUtil: InputMethodUtil) {
|
||||
self.keymanVersion = version
|
||||
public init(defaultsRepo: DefaultsRepo, inputMethodUtil: InputMethodUtil) {
|
||||
self.defaultsRepository = defaultsRepo
|
||||
self.inputMethodUtil = inputMethodUtil
|
||||
let exists = inputMethodUtil.keymanInputMethodExists()
|
||||
|
||||
if exists {
|
||||
self.inputMethodExists = true
|
||||
self.inputMethodVersion = (try? inputMethodUtil.getKeymanInputMethodVersion()) ?? "unknown"
|
||||
} else {
|
||||
self.inputMethodExists = false
|
||||
self.inputMethodVersion = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
public func evaluate() -> InstallationState {
|
||||
var installationState: InstallationState
|
||||
|
||||
guard self.inputMethodExists else {
|
||||
return createInstallationStateForNonexistentInputMethod()
|
||||
}
|
||||
|
||||
if let savedInstallationState = readInstallationState() {
|
||||
// check whether the installation requires repair
|
||||
if let repairInstallationState = self.createRepairInstallationState(savedInstallationState: savedInstallationState) {
|
||||
|
|
@ -34,8 +47,7 @@ public class InstallationCheck {
|
|||
} else {
|
||||
installationState = savedInstallationState
|
||||
}
|
||||
} else
|
||||
{
|
||||
} else {
|
||||
// if installation could not be read, then
|
||||
installationState = self.createInstallationStateForNewInstallation()
|
||||
}
|
||||
|
|
@ -61,12 +73,23 @@ public class InstallationCheck {
|
|||
func checkForNewInstallation() {
|
||||
// MAC-CONFIG-TODO: impelement version checks
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create InstallationState for non-existent input method (installer needs to be run)
|
||||
* Note that this is not persisted, because a non-existent installation doesn't really have state.
|
||||
* Instead this is used to indicate to the user that they need to run the installer and try again.
|
||||
*/
|
||||
func createInstallationStateForNonexistentInputMethod() -> InstallationState {
|
||||
var taskList = Set<InstallationTask>()
|
||||
taskList.insert(InstallationTask(task: .verifyInputMethod, completed: false))
|
||||
return InstallationState(exists: false, version: "unknown", tasks: taskList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a InstallationState object describing a new installation
|
||||
*/
|
||||
func createInstallationStateForNewInstallation() -> InstallationState {
|
||||
let installationState = InstallationState(version: self.keymanVersion, tasks: self.createNewInstallationTasks())
|
||||
let installationState = InstallationState(version: self.inputMethodVersion, tasks: self.createNewInstallationTasks())
|
||||
self.defaultsRepository.writeInstallationState(installationState.toUserDefaultsDictionary())
|
||||
|
||||
return installationState
|
||||
|
|
@ -112,7 +135,7 @@ public class InstallationCheck {
|
|||
}
|
||||
|
||||
if !repairTasks.isEmpty {
|
||||
repairInstallationState = InstallationState(version: self.keymanVersion, tasks: repairTasks)
|
||||
repairInstallationState = InstallationState(version: self.inputMethodVersion, tasks: repairTasks)
|
||||
}
|
||||
|
||||
return repairInstallationState
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ public class InstallationContainer : ObservableObject {
|
|||
self.defaultsRepository = defaultsRepo
|
||||
inputMethodUtil = InputMethodUtil()
|
||||
notificationCenter = DistributedNotificationCenter.default()
|
||||
|
||||
self.installationState = InstallationCheck(version: "1.0.0", defaultsRepo: defaultsRepo, inputMethodUtil: inputMethodUtil).evaluate()
|
||||
|
||||
self.installationState = InstallationCheck(defaultsRepo: defaultsRepo, inputMethodUtil: inputMethodUtil).evaluate()
|
||||
|
||||
self.registerObservers()
|
||||
}
|
||||
|
|
@ -212,7 +212,7 @@ public class InstallationContainer : ObservableObject {
|
|||
* for testing purposes, replace the InstallationState with a new object set for a new installation
|
||||
*/
|
||||
func resetInstallation() {
|
||||
self.installationState = InstallationCheck(version: "1.0.0", defaultsRepo: self.defaultsRepository, inputMethodUtil: self.inputMethodUtil).createInstallationStateForNewInstallation()
|
||||
self.installationState = InstallationCheck(defaultsRepo: self.defaultsRepository, inputMethodUtil: self.inputMethodUtil).createInstallationStateForNewInstallation()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -234,11 +234,11 @@ public class InstallationContainer : ObservableObject {
|
|||
}
|
||||
|
||||
public func debug() {
|
||||
let version = inputMethodUtil.getKeymanInputMethodVersion()
|
||||
let version = (try? inputMethodUtil.getKeymanInputMethodVersion()) ?? "unknown"
|
||||
let enabled = inputMethodUtil.isKeymanInputMethodEnabled()
|
||||
let running = inputMethodUtil.isKeymanInputMethodRunning()
|
||||
let accessGranted = self.accessIsGranted()
|
||||
print("Keyman status, version: \(version ?? ""), enabled: \(enabled), running: \(running), accessGranted: \(accessGranted)")
|
||||
print("Keyman status, version: \(version), enabled: \(enabled), running: \(running), accessGranted: \(accessGranted)")
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -314,11 +314,12 @@ public class InstallationContainer : ObservableObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* first kill the Keyman input method if it is running
|
||||
* second, call Keyman as a separate process with an argument that requests the system to prompt the user to grant accessibility
|
||||
* listen for message from Keyman to indicate the result
|
||||
* First kill the Keyman input method if it is running.
|
||||
* Second, call Keyman as a separate process with an argument that requests the system to prompt the user to grant accessibility.
|
||||
* The Keyman input method cannot send a message to indicate success, because it does not know itself when the user has
|
||||
* finished making the change in Settings.
|
||||
* To learn the result, we must poll with `isAccessibilityGranted()`
|
||||
*/
|
||||
// MAC-CONFIG-TODO: do we get an accurate message from Keyman Input Method
|
||||
public func requestAccessibility() -> Bool {
|
||||
var requested = false
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public class InstallationState {
|
|||
let kDateRestartRequestedKey = "dateRestartRequested"
|
||||
let kRepairKey = "isRepair"
|
||||
|
||||
public let inputMethodExists: Bool
|
||||
public let keymanVersion: String
|
||||
public var dateRestartRequested: Date?
|
||||
// indicates whether we are repairing a previous installation or doing a full installation
|
||||
|
|
@ -27,20 +28,8 @@ public class InstallationState {
|
|||
tasks.allSatisfy(\.isComplete)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create InstallationState for new install
|
||||
*/
|
||||
static func createForNewInstall(version: String) -> InstallationState {
|
||||
var installationTasks = Set<InstallationTask>()
|
||||
installationTasks.insert(InstallationTask(task: .migrateData, completed: false))
|
||||
installationTasks.insert(InstallationTask(task: .enableInputMethod, completed: false))
|
||||
installationTasks.insert(InstallationTask(task: .requestAccess, completed: false))
|
||||
installationTasks.insert(InstallationTask(task: .restartMac, completed: false))
|
||||
|
||||
return InstallationState(version: version, tasks: installationTasks)
|
||||
}
|
||||
|
||||
init(version: String, dateRestartRequested: Date? = nil, isRepair: Bool = false, tasks: Set<InstallationTask>) {
|
||||
init(exists: Bool = true, version: String, dateRestartRequested: Date? = nil, isRepair: Bool = false, tasks: Set<InstallationTask>) {
|
||||
self.inputMethodExists = exists
|
||||
self.keymanVersion = version
|
||||
self.dateRestartRequested = dateRestartRequested
|
||||
self.isRepair = isRepair
|
||||
|
|
@ -51,6 +40,8 @@ public class InstallationState {
|
|||
* initialize using the dictionary from UserDefaults
|
||||
*/
|
||||
init?(from dictionary: Dictionary<String, Any>) {
|
||||
// assume that the input method exists, as we only attempt to load this data when it does
|
||||
self.inputMethodExists = true
|
||||
self.keymanVersion = dictionary[kVersionKey] as? String ?? ""
|
||||
self.dateRestartRequested = dictionary[kDateRestartRequestedKey] as? Date
|
||||
self.isRepair = dictionary[kRepairKey] as? Bool ?? false
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ function archive_apps() {
|
|||
-configuration Release \
|
||||
-archivePath "${KEYMAN_BUILD_PATH}/Keyman.xcarchive" \
|
||||
ARCHS=\"arm64 x86_64\" \
|
||||
ONLY_ACTIVE_ARCH=NO
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
PRODUCT_VERSION=$KEYMAN_VERSION
|
||||
|
||||
# xcodebuild for x86_64 and arm64 (universal binary)
|
||||
mac_xcodebuild archive -workspace "$KEYMAN_WORKSPACE_PATH" \
|
||||
|
|
@ -70,7 +71,8 @@ function archive_apps() {
|
|||
-configuration Release \
|
||||
-archivePath "${KEYMAN_BUILD_PATH}/Config.xcarchive" \
|
||||
ARCHS=\"arm64 x86_64\" \
|
||||
ONLY_ACTIVE_ARCH=NO
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
PRODUCT_VERSION=$KEYMAN_VERSION
|
||||
}
|
||||
|
||||
function export_apps() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue