spiegel-keyman/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift
Shawn Schantz bb816a1335
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled
feat(mac): display packages and keyboard list view
added buttons for removal of packages
and toggle for disabling of keyboard
not remotely final but mostly for ensuring that
we can bind to the data model and respond to updates
2026-04-02 14:01:11 -04:00

186 lines
6.7 KiB
Swift

/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2025-12-10
*
* DataRepository is responsible for reading, writing and removing
* Keyman data on disk.
*
*/
import Foundation
public struct PackageRepository {
fileprivate let packageFileName = "kmp.json"
fileprivate let pathUtil: KeymanPaths
public init() {
self.pathUtil = KeymanPaths()
}
func loadPackages() -> [KeymanPackage] {
var installedPackages: [KeymanPackage] = []
let packageSourceArray = self.readKeymanPackagesForKeyman19()
// create a KeymanPackage object for each PackageSource object and insert it in the array if it is valid
for source in packageSourceArray {
let package = KeymanPackage(packageSource: source)
if (package.validate()) {
installedPackages.append(package)
} else {
print("package '\(source.packageName)' is not valid")
}
}
return installedPackages
}
/**
* Creates the directory tree where keyboards are stored under the standard 'Group Containers' directory
*/
func createKeyman19SharedDataDirectories() {
if let keyboardDirectory = pathUtil.keyman19KeyboardsDirectory {
do {
// create the directory if it doesn't already exist
if !FileManager.default.fileExists(atPath: keyboardDirectory.path) {
try FileManager.default.createDirectory(at: keyboardDirectory, withIntermediateDirectories: true, attributes: nil)
// ConfigLogger.shared.testLogger.debug("Created directory: \(keyboardDirectory.path)")
print("Created directory: \(keyboardDirectory.path)")
} else {
// ConfigLogger.shared.testLogger.debug("Directory already exists: \(keyboardDirectory.path)")
print("Directory already exists: \(keyboardDirectory.path)")
}
} catch {
// ConfigLogger.shared.testLogger.error("Error creating directory: \(error.localizedDescription)")
print("Error creating directory: \(error.localizedDescription)")
}
} else {
// ConfigLogger.shared.testLogger.error("Unable to create directory, Group Container URL not found.")
print("Unable to create directory, Group Container URL not found.")
}
}
/**
* for group container testing purposes to check directory access
*/
public func writeSomethingToContainer() {
if let keyboardDirectory = self.pathUtil.keyman19KeyboardsDirectory {
print("About to write to: \(keyboardDirectory.path)")
do {
// Example: Save a file to the shared directory
let fileURL = keyboardDirectory.appendingPathComponent("bogus-kmp.txt")
let data = "Not a .kmp file".data(using: .utf8)
try data?.write(to: fileURL)
print("Wrote data to shared file: \(fileURL.path)")
} catch {
print("Error writing file to shared directory: \(error.localizedDescription)")
}
} else {
print("App Group container URL not found. Check your entitlements and provisioning profiles.")
}
}
/**
* Check to see whether the shared Keyman data directory exists under 'Library/Group Containers/'
*/
public func keyman19SharedDataDirectoryExists() -> Bool {
guard let keyboardsUrl = self.pathUtil.keyman19KeyboardsDirectory else {
return false
}
return self.directoryExistsAtPath(directoryUrl: keyboardsUrl)
}
/**
* returns true if a directory exists at the specified URL
*/
func directoryExistsAtPath(directoryUrl: URL) -> Bool {
var isDirectory: ObjCBool = false
let exists = FileManager.default.fileExists(atPath: directoryUrl.path, isDirectory: &isDirectory)
return exists && isDirectory.boolValue
}
public func readKeymanPackagesForKeyman19() -> [PackageSource] {
guard let keyboardsUrl = self.pathUtil.keyman19KeyboardsDirectory else {
return []
}
// print("App Group container URL not found. Check your entitlements and provisioning profiles.")
return readKeyboardPackageSource(keyboardDirectoryUrl: keyboardsUrl)
}
public func readKeymanPackagesForKeyman18() -> [PackageSource] {
guard let keyboardsUrl = self.pathUtil.keyman18KeyboardsDirectory else {
return []
}
return readKeyboardPackageSource(keyboardDirectoryUrl: keyboardsUrl)
}
public func readKeyboardPackageSource(keyboardDirectoryUrl: URL) -> [PackageSource] {
var packages: [PackageSource] = []
do {
// Get the URLs for all items in the directory that are not hidden
let directoryContents = try FileManager.default.contentsOfDirectory(
at: keyboardDirectoryUrl,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
)
for itemUrl in directoryContents {
// if the item is a directory, then attempt to read it as a keyboard package
if (itemUrl.hasDirectoryPath) {
if let packageSource = readKeyboardPackageFromDirectory(keyboardDirectoryUrl: itemUrl) {
packages.append(packageSource)
}
}
}
} catch {
print("Failed to read directory: \(error.localizedDescription)")
}
print("\(packages.count) packages read")
return packages
}
func readKeyboardPackageFromDirectory(keyboardDirectoryUrl: URL) -> PackageSource? {
var packageSource: PackageSource?
let lastPathComponent = keyboardDirectoryUrl.lastPathComponent
let kmpFileUrl = keyboardDirectoryUrl.appendingPathComponent(packageFileName)
if FileManager.default.fileExists(atPath: kmpFileUrl.path) {
if let source = readKeyboardPackage(kmpFileUrl) {
packageSource = source
// save the keyboard directory and path of the kmp.json for this keyboard
packageSource?.directoryUrl = keyboardDirectoryUrl
packageSource?.jsonFileUrl = kmpFileUrl
}
} else {
// ConfigLogger.shared.testLogger.debug(" ** \(lastPathComponent) DOES NOT contain the file 'kmp.json')")
print(" ** \(lastPathComponent) DOES NOT contain the file 'kmp.json')")
}
return packageSource
}
public func readKeyboardPackage(_ kmpFileUrl: URL) -> PackageSource? {
var packageSource: PackageSource?
do {
let jsonData = try Data(contentsOf: kmpFileUrl, options: .mappedIfSafe)
let source: PackageSource = try! JSONDecoder().decode(PackageSource.self, from: jsonData)
// ConfigLogger.shared.testLogger.debug("package name: \(source.packageName)")
print("package name: \(source.packageName)")
packageSource = source
} catch {
// ConfigLogger.shared.testLogger.debug("package name: \(source.packageName)")
// print("package name: \(source.packageName)")
}
return packageSource
}
}