Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5089ce4
feat(mac): define InstallationPhase enum for UI
sgschantz Jul 20, 2026
7c75e1a
feat(mac): pre-check install state before creating task list
sgschantz Jul 23, 2026
d4f36f4
feat(mac): pre-install evaluation of install state
sgschantz Jul 24, 2026
6f76e08
feat(mac): add hasTasks flag to InstallationPhase
sgschantz Jul 24, 2026
e99c917
feat(mac): stop attempting to read version of non-existent input method
sgschantz Jul 24, 2026
4219ebe
feat(mac): added confirm access step
sgschantz Jul 25, 2026
9ac6099
feat(mac): added confirmRestart state
sgschantz Jul 27, 2026
c9ef73c
feat(mac): create new InstallationState whenever task is completed
sgschantz Jul 27, 2026
172d577
feat(mac): removed obsolete TODO comments
sgschantz Jul 27, 2026
5b0b88a
feat(mac): create new installation state for all state changes
sgschantz Jul 27, 2026
724136e
feat(mac): remove duplicated InstallationState property
sgschantz Jul 27, 2026
fdcb0bc
feat(mac): update comment
sgschantz Jul 27, 2026
a419df3
feat(mac): added prepareRepair state
sgschantz Jul 28, 2026
ddba07d
feat(mac): refresh install state if version is out of date
sgschantz Jul 28, 2026
ae1a734
feat(mac): rename test/debug views
sgschantz Jul 29, 2026
cfd0d15
feat(mac): only update for confirmAccess if it is current task
sgschantz Jul 29, 2026
948bc84
Merge branch 'feat/mac/expand-package' of https://github.com/keymanap…
sgschantz Aug 5, 2026
f5bc2e9
Merge branch 'epic/mac-config' into feat/mac/install-notifications
sgschantz Aug 6, 2026
5444423
feat(mac): update for review comments
sgschantz Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions mac/Config/Config/ConfigApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,12 @@ struct ConfigApp: App {
@Environment(\.openWindow) private var openWindow

var body: some Scene {
Window("Configuration", id: "config") {
ConfigView()
Window("Config Test", id: "config-debug") {
ConfigDebugView()
.environmentObject(settings)
.task {
if !installation.isInstallationComplete() {
openWindow(id: "install")
}
}
}
Window("Installation", id: "install") {
InstallView()
Window("Install Test", id: "install-debug") {
InstallDebugView()
.environmentObject(installation)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
*
* Created by Shawn Schantz on 2026-02-26
*
* Main view used for configuring Keyman
* View for debugging Keyman configuration
*/

import SwiftUI
import KeymanSettings

struct ConfigView: View {
struct ConfigDebugView: View {
@EnvironmentObject var settings: SettingsContainer
@State private var isShowingSheet = false

Expand Down Expand Up @@ -86,7 +86,7 @@ struct ConfigView: View {
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
}
KeyboardListView(packageId: package.id, keyboards: package.keyboards)
KeyboardListDebugView(packageId: package.id, keyboards: package.keyboards)
}
}
}
Expand All @@ -99,6 +99,6 @@ struct ConfigView: View {

#Preview {
let settings = SettingsContainer()
ConfigView()
ConfigDebugView()
.environmentObject(settings)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,29 @@

import SwiftUI

struct InstallView: View {
struct InstallDebugView: View {
@EnvironmentObject var installation: InstallationContainer

@State private var taskText: String = "[task]"

var body: some View {
VStack {
HStack {
Image(systemName: "gear")
.imageScale(.large)
.foregroundColor(.accentColor)
if let nextTask = installation.nextTask() {
Text("Next task = \(nextTask.taskType.rawValue)")
}
Text("Current task = \(taskText)")
.onAppear() {
if let installTask = installation.currentTask() {
taskText = installTask.taskType.rawValue
}
}
}
HStack {
Button("Next...") {
installation.executeNextInstallationTask()
installation.executeCurrentInstallationTask()
if let installTask = installation.currentTask() {
taskText = installTask.taskType.rawValue
}
}
.disabled(installation.isInstallationComplete())
Button("Migrate Data") {
Expand Down Expand Up @@ -54,6 +61,12 @@ struct InstallView: View {
Button("Check Restart") {
_ = installation.validateUserHasRestarted()
}
Button("Set Displayed Complete") {
let beforeDisplayed = installation.getHasDisplayedInstallationComplete()
installation.setHasDisplayedInstallationComplete()
let afterDisplayed = installation.getHasDisplayedInstallationComplete()
print("hasDisplayedInstallComplete = \(beforeDisplayed) -> \(afterDisplayed)")
}
Button("debug") {
installation.debug()
}
Expand All @@ -66,22 +79,17 @@ struct InstallView: View {
Button("Uninstall") {
installation.uninstall()
}
Button("Force Reset Installation") {
installation.forceResetInstallation()
}
Button("Force Validate Installation") {
installation.forceValidateInstallation()
}
Spacer()
}
.padding()
}
.padding()
// .onReceive(NotificationCenter.default.publisher(for: .inputMethodMissing), perform: {_ in print("input method missing")})
}
}

#Preview {
let installation = InstallationContainer()
InstallView()
InstallDebugView()
.environmentObject(installation)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SwiftUI
import KeymanSettings
import Combine

struct KeyboardListView: View {
struct KeyboardListDebugView: View {
@EnvironmentObject var settings: SettingsContainer
@State var packageId: UUID
@State var keyboards: [Keyboard]
Expand Down
33 changes: 16 additions & 17 deletions mac/Config/Installation/InputMethodUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,6 @@ public class InputMethodUtil {
return FileManager.default.fileExists(atPath: inputMethodUrl.path)
}

/**
* true if the Keyman input method of the correct version exists in the correct location
*/
public func isKeymanInputMethodCurrent() -> Bool {
// MAC-CONFIG-TODO: implement with version check
return true
}

/**
* Returns version number string of Keyman input method
*/
Expand Down Expand Up @@ -177,6 +169,11 @@ public class InputMethodUtil {
var success = false
do {
print("invokeKeymanInputMethodRequestAccess()")

// because we are launching Keyman with a specific command line argument
// for this request, we must kill it first
_ = self.killKeymanInputMethod()

try self.launchKeymanInputMethodAsSeparateProcess(argument: kAccessCommand)
success = true
} catch {
Expand All @@ -187,14 +184,19 @@ public class InputMethodUtil {
}

/**
* Calls Keyman input to check whether it has accessibility permission granted.
* Calls Keyman input method to check whether it has accessibility permission granted.
* The actual result is not returned from Keyman when called as a separate process.
* After this function is called, listen to the `NotificationCenter` for the notification named
* `com.keyman.accessibility.state`
* After this function is called, listen to the `DistributedNotificationCenter` for the notification named
* `accessibilityStateResponse`
* It contains a message with a value of `granted` or `not-granted`
*/
func invokeKeymanInputMethodCheckAccess() throws {
print("invokeKeymanInputMethodCheckAccess()")

// because we are launching Keyman with a specific command line argument
// for this request, we must kill it first
_ = self.killKeymanInputMethod()

try self.launchKeymanInputMethodAsSeparateProcess(argument: kCheckCommand)
}

Expand Down Expand Up @@ -250,11 +252,8 @@ public class InputMethodUtil {
}

/**
* Special care is needed with this code because `processAccessCheckResult(with:)` is bound to the Main Actor,
* but it is called from a closure which may not run on the Main Actor.
* DistributedNotificationCenter is not fully updated for concurrency, so specifying `.main` for the `OperationQueue`
* does not ensure that the closure is running on the main actor.
* To ensure this, a Task is defined around the call to `processAccessCheckResult`.
* Calls Keyman input method to check whether it has accessibility permission granted.
* Receives response as distributed notification named `accessibilityStateResponse`
*/
func doAsyncAccessibilityCheck() {
do {
Expand All @@ -273,7 +272,7 @@ public class InputMethodUtil {

/**
* Kill the application with the specified bundle Id
* This is only permitted when running oustide sandbox
* This is only permitted when running outside sandbox
*/
func killApplication(bundleId: String) -> Bool {
let runningApps = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId)
Expand Down
Loading