vChewing-macOS/Packages/vChewing_MainAssembly/Sources/MainAssembly/LangModelManager/LMMgr_Utilities.swift

348 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import BookmarkManager
import LangModelAssembly
import Shared
import SwiftExtension
/// 使
private let kTemplateNameUserPhrases = "template-userphrases"
private let kTemplateNameUserReplacements = "template-replacements"
private let kTemplateNameUserFilterList = "template-exclusions"
private let kTemplateNameUserSymbolPhrases = "template-usersymbolphrases"
private let kTemplateNameUserAssociatesCHS = "template-associatedPhrases-chs"
private let kTemplateNameUserAssociatesCHT = "template-associatedPhrases-cht"
public extension LMMgr {
// MARK: - Containers
// 使
static func getBundleDataPath(_ filenameSansExt: String, factory: Bool = false, ext: String) -> String? {
let factoryPath = Bundle.main.path(forResource: filenameSansExt, ofType: ext) ?? Bundle.module.path(forResource: filenameSansExt, ofType: ext)
guard let factoryPath = factoryPath else { return nil }
let factory = PrefMgr.shared.useExternalFactoryDict ? factory : true
let containerPath = Self.appSupportURL.appendingPathComponent("vChewingFactoryData/\(filenameSansExt).\(ext)").path
.expandingTildeInPath
var isFailed = false
if !factory {
var isFolder = ObjCBool(false)
if !FileManager.default.fileExists(atPath: containerPath, isDirectory: &isFolder) { isFailed = true }
if !isFailed, !FileManager.default.isReadableFile(atPath: containerPath) { isFailed = true }
}
let result = (factory || isFailed) ? factoryPath : containerPath
return result
}
// MARK: - SQLite Containers
static func getCoreDictionaryDBPath(factory: Bool = false) -> String? {
guard let factoryResultURL = Bundle.main.url(forResource: "vChewingFactoryDatabase", withExtension: "sqlite") else {
return nil
}
guard !factory, PrefMgr.shared.useExternalFactoryDict else { return factoryResultURL.path }
let containerResultURL = Self.appSupportURL.appendingPathComponent("vChewingFactoryData/vChewingFactoryDatabase.sqlite")
return FileManager.default.fileExists(atPath: containerResultURL.path, isDirectory: nil) ? containerResultURL.path : factoryResultURL.path
}
// MARK: - 使
// Swift appendingPathComponent URL
/// 使
/// - Parameters:
/// - mode:
/// - type:
/// - Returns: URL
static func userDictDataURL(mode: Shared.InputMode, type: vChewingLM.ReplacableUserDataType) -> URL {
var fileName: String = {
switch type {
case .thePhrases: return "userdata"
case .theFilter: return "exclude-phrases"
case .theReplacements: return "phrases-replacement"
case .theAssociates: return "associatedPhrases"
case .theSymbols: return "usersymbolphrases"
}
}()
fileName.append((mode == .imeModeCHT) ? "-cht.txt" : "-chs.txt")
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
}
/// 使
/// - Returns: URL
static func userSymbolMenuDataURL() -> URL {
let fileName = "symbols.dat"
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName)
}
/// 使使
/// ~/Library/Application Support/vChewing/使
/// - Parameter mode:
/// - Returns: URL
static func userOverrideModelDataURL(_ mode: Shared.InputMode) -> URL {
let fileName: String = {
switch mode {
case .imeModeCHS: return "vChewing_override-model-data-chs.dat"
case .imeModeCHT: return "vChewing_override-model-data-cht.dat"
case .imeModeNULL: return "vChewing_override-model-data-dummy.dat"
}
}()
return URL(
fileURLWithPath: dataFolderPath(isDefaultFolder: true)
).deletingLastPathComponent().appendingPathComponent(fileName)
}
// MARK: - 使
//
static func checkIfSpecifiedUserDataFolderValid(_ folderPath: String?) -> Bool {
var isFolder = ObjCBool(false)
let folderExist = FileManager.default.fileExists(atPath: folderPath ?? "", isDirectory: &isFolder)
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
//
//
var folderPath = folderPath // Convert the incoming constant to a variable.
if isFolder.boolValue {
folderPath?.ensureTrailingSlash()
}
let isFolderWritable = FileManager.default.isWritableFile(atPath: folderPath ?? "")
// vCLog("mgrLM: Exist: \(folderExist), IsFolder: \(isFolder.boolValue), isWritable: \(isFolderWritable)")
if ((folderExist && !isFolder.boolValue) || !folderExist) || !isFolderWritable {
return false
}
return true
}
//
static func checkCassettePathValidity(_ cassettePath: String?) -> Bool {
var isFolder = ObjCBool(true)
let isExist = FileManager.default.fileExists(atPath: cassettePath ?? "", isDirectory: &isFolder)
// The above "&" mutates the "isFolder" value to the real one received by the "isExist".
let isReadable = FileManager.default.isReadableFile(atPath: cassettePath ?? "")
return !isFolder.boolValue && isExist && isReadable
}
//
static var userDataFolderExists: Bool {
let folderPath = Self.dataFolderPath(isDefaultFolder: false)
var isFolder = ObjCBool(false)
var folderExist = FileManager.default.fileExists(atPath: folderPath, isDirectory: &isFolder)
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
//
//
//
if folderExist, !isFolder.boolValue {
do {
if dataFolderPath(isDefaultFolder: false)
== dataFolderPath(isDefaultFolder: true)
{
let formatter = DateFormatter()
formatter.dateFormat = "YYYYMMDD-HHMM'Hrs'-ss's'"
let dirAlternative = folderPath + formatter.string(from: Date())
try FileManager.default.moveItem(atPath: folderPath, toPath: dirAlternative)
} else {
throw folderPath
}
} catch {
print("Failed to make path available at: \(error)")
return false
}
folderExist = false
}
if !folderExist {
do {
try FileManager.default.createDirectory(
atPath: folderPath,
withIntermediateDirectories: true,
attributes: nil
)
} catch {
print("Failed to create folder: \(error)")
return false
}
}
return true
}
// MARK: - 使 PrefMgr
// PrefMgr
static let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
static func dataFolderPath(isDefaultFolder: Bool) -> String {
var userDictPathSpecified = PrefMgr.shared.userDataFolderSpecified.expandingTildeInPath
var userDictPathDefault =
Self.appSupportURL.appendingPathComponent("vChewing").path.expandingTildeInPath
userDictPathDefault.ensureTrailingSlash()
userDictPathSpecified.ensureTrailingSlash()
if (userDictPathSpecified == userDictPathDefault)
|| isDefaultFolder
{
return userDictPathDefault
}
if UserDefaults.current.object(forKey: UserDef.kUserDataFolderSpecified.rawValue) != nil {
BookmarkManager.shared.loadBookmarks()
if Self.checkIfSpecifiedUserDataFolderValid(userDictPathSpecified) {
return userDictPathSpecified
}
UserDefaults.current.removeObject(forKey: UserDef.kUserDataFolderSpecified.rawValue)
}
return userDictPathDefault
}
static func cassettePath() -> String {
let rawCassettePath = PrefMgr.shared.cassettePath
if UserDefaults.current.object(forKey: UserDef.kCassettePath.rawValue) != nil {
BookmarkManager.shared.loadBookmarks()
if Self.checkCassettePathValidity(rawCassettePath) { return rawCassettePath }
UserDefaults.current.removeObject(forKey: UserDef.kCassettePath.rawValue)
}
return ""
}
// MARK: - 使
static func resetSpecifiedUserDataFolder() {
UserDefaults.current.set(dataFolderPath(isDefaultFolder: true), forKey: UserDef.kUserDataFolderSpecified.rawValue)
Self.initUserLangModels()
}
static func resetCassettePath() {
UserDefaults.current.set("", forKey: UserDef.kCassettePath.rawValue)
Self.loadCassetteData()
}
// MARK: - 使
static func writeUserPhrasesAtOnce(
_ userPhrase: UserPhrase, areWeFiltering: Bool,
errorHandler: (() -> Void)? = nil
) {
let resultA = userPhrase.write(toFilter: areWeFiltering)
let resultB = userPhrase.crossConverted.write(toFilter: areWeFiltering)
guard resultA, resultB else {
if let errorHandler = errorHandler {
errorHandler()
}
return
}
// The new FolderMonitor module does NOT monitor cases that files are modified
// by the current application itself, requiring additional manual loading process here.
if PrefMgr.shared.phraseEditorAutoReloadExternalModifications {
Broadcaster.shared.eventForReloadingPhraseEditor = .init()
}
loadUserPhrasesData(type: areWeFiltering ? .theFilter : .thePhrases)
}
// MARK: - 使
private static func checkIfUserFilesExistBeforeOpening() -> Bool {
if !Self.chkUserLMFilesExist(.imeModeCHS)
|| !Self.chkUserLMFilesExist(.imeModeCHT)
{
let content = String(
format: NSLocalizedString(
"Please check the permission at \"%@\".", comment: ""
),
Self.dataFolderPath(isDefaultFolder: false)
)
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Unable to create the user phrase file.", comment: "")
alert.informativeText = content
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.runModal()
NSApp.popup()
}
return false
}
return true
}
static func openUserDictFile(type: vChewingLM.ReplacableUserDataType, dual: Bool = false, alt: Bool) {
let app: FileOpenMethod = alt ? .textEdit : .finder
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode, type: type), using: app)
guard dual else { return }
openPhraseFile(fromURL: userDictDataURL(mode: IMEApp.currentInputMode.reversed, type: type), using: app)
}
///
/// - Parameters:
/// - url: URL
/// - FileOpenMethod: App
static func openPhraseFile(fromURL url: URL, using app: FileOpenMethod) {
if !Self.checkIfUserFilesExistBeforeOpening() { return }
DispatchQueue.main.async {
app.open(url: url)
}
}
// MARK: - 使
private static func ensureFileExists(
_ fileURL: URL, deployTemplate templateBasename: String = "1145141919810",
extension ext: String = "txt"
) -> Bool {
let filePath = fileURL.path
if !FileManager.default.fileExists(atPath: filePath) {
let templateURL = Bundle.main.url(forResource: templateBasename, withExtension: ext)
var templateData = Data("".utf8)
if templateBasename != "" {
do {
try templateData = Data(contentsOf: templateURL ?? URL(fileURLWithPath: ""))
} catch {
templateData = Data("".utf8)
}
do {
try templateData.write(to: URL(fileURLWithPath: filePath))
} catch {
vCLog("Failed to write template data to: \(filePath)")
return false
}
}
}
return true
}
@discardableResult static func chkUserLMFilesExist(_ mode: Shared.InputMode) -> Bool {
if !userDataFolderExists {
return false
}
/// CandidateNode UserOverrideModel
///
///
var failed = false
caseCheck: for type in vChewingLM.ReplacableUserDataType.allCases {
let templateName = Self.templateName(for: type, mode: mode)
if !ensureFileExists(userDictDataURL(mode: mode, type: type), deployTemplate: templateName) {
failed = true
break caseCheck
}
}
return !failed
}
internal static func templateName(for type: vChewingLM.ReplacableUserDataType, mode: Shared.InputMode) -> String {
switch type {
case .thePhrases: return kTemplateNameUserPhrases
case .theFilter: return kTemplateNameUserFilterList
case .theReplacements: return kTemplateNameUserReplacements
case .theSymbols: return kTemplateNameUserSymbolPhrases
case .theAssociates:
return mode == .imeModeCHS ? kTemplateNameUserAssociatesCHS : kTemplateNameUserAssociatesCHT
}
}
}