diff --git a/.github/README.md b/.github/README.md index 03cb636..29fce6b 100644 --- a/.github/README.md +++ b/.github/README.md @@ -4,12 +4,23 @@ Easily type Indian languages on macOS using [Varnam transliteration engine](http Built at FOSSUnited's [FOSSHack21](https://fossunited.org/fosshack/2021/project?project=Type%20Indian%20Languages%20natively%20on%20Mac). -This project is a hard-fork of [lipika-ime](github.com/ratreya/Lipika_IME). Changes made: +This project is a hard-fork of [lipika-ime](https://github.com/ratreya/Lipika_IME). Changes made: * https://github.com/varnamproject/varnam-macOS/pull/1 +There aren't many documentation on how to make IMEs for macOS, especially in **English**. Getting started with XCode is also tricky for beginners. Setting up **Lipika** was also difficult. + +Resources that helped in making IME on macOS (ordered by most important to the least): +* https://blog.inoki.cc/2021/06/19/Write-your-own-IME-on-macOS-1/ (The last section is very important!) +* https://jyhong836.github.io/tech/2015/07/29/add-3rd-part-dynamic-library-dylib-to-xcode-target.html +* https://github.com/lennylxx/google-input-tools-macos (An IME made 2 months ago, Has GitHub CI builds) +* https://github.com/nh7a/hiragany (Simple Japanese IME) +* https://github.com/pkamb/NumberInput_IMKit_Sample/issues/1 +* API Docs: https://developer.apple.com/documentation/inputmethodkit/imkcandidates + ## License > Copyright (C) 2018 Ranganath Atreya +> > Copyright (C) 2021 Subin Siby ``` @@ -18,4 +29,4 @@ General Public License as published by the Free Software Foundation; either vers or (at your option) any later version. This program comes with ABSOLUTELY NO WARRANTY; see LICENSE file. -``` \ No newline at end of file +``` diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d5900aa --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "GoVarnam/govarnam"] + path = GoVarnam/govarnam + url = git@github.com:varnamproject/govarnam.git diff --git a/Application/LiteratorView.swift b/Application/LiteratorView.swift index 7e39b37..881e3a2 100644 --- a/Application/LiteratorView.swift +++ b/Application/LiteratorView.swift @@ -87,7 +87,7 @@ class LiteratorModel: ObservableObject { return self.ante!.anteliterate(lit.finalaizedOutput + lit.unfinalaizedOutput) } case (0, 1): - let override: [String: MappingValue]? = MappingStore.read(schemeName: self.fromScheme, scriptName: self.toScript) + let override: [String: MappingValue]? = .read(schemeName: self.fromScheme, scriptName: self.toScript) self.trans = try! self.factory.transliterator(schemeName: self.fromScheme, scriptName: self.toScript, mappings: override) eval = { (input: String) -> String in _ = self.trans!.reset() diff --git a/GoVarnam/.gitignore b/GoVarnam/.gitignore new file mode 100644 index 0000000..e5d0467 --- /dev/null +++ b/GoVarnam/.gitignore @@ -0,0 +1 @@ +*.dylib diff --git a/GoVarnam/Varnam.swift b/GoVarnam/Varnam.swift new file mode 100644 index 0000000..6c5f429 --- /dev/null +++ b/GoVarnam/Varnam.swift @@ -0,0 +1,62 @@ +// +// Varnam.swift +// VarnamIME +// +// Created by Subin on 13/11/21. +// Copyright © 2021 VarnamProject. All rights reserved. +// + +import Foundation + +struct VarnamException: Error { + let message: String + + init(_ message: String) { + self.message = message + } + + public var localizedDescription: String { + return message + } +} + +public class Varnam { + private var varnamHandle: Int32 = 0; + + internal init(_ schemeID: String = "ml") throws { + schemeID.withCString { + let rc = varnam_init_from_id(UnsafeMutablePointer(mutating: $0), &varnamHandle) + try! checkError(rc) + } + } + + public func getLastError() -> String { + return String(cString: varnam_get_last_error(varnamHandle)) + } + + public func checkError(_ rc: Int32) throws { + if (rc != VARNAM_SUCCESS) { + throw VarnamException(getLastError()) + } + } + + public func transliterate(_ input: String) -> [String] { + var arr: UnsafeMutablePointer? = varray_init() + let cInput = (input as NSString).utf8String + varnam_transliterate( + varnamHandle, + 1, + UnsafeMutablePointer(mutating: cInput), + &arr + ) + + var results = [String]() + for i in (0.. Bool { - Logger.log.debug("Cursor moved: \(delta) with selectedRange: \(client.selectedRange()), markedRange: \(client.markedRange()) and cursorPosition: \(markedCursorLocation?.description ?? "nil")") - if client.markedRange().length == NSNotFound { return false } - let nextPosition = (markedCursorLocation ?? client.markedRange().length) + delta - if (0...client.markedRange().length).contains(nextPosition) { - Logger.log.debug("Still within markedRange") - markedCursorLocation = nextPosition - return true - } - Logger.log.debug("Outside of markedRange") - markedCursorLocation = nil - return false + func updatePreedit(_ text: NSAttributedString, _ cursorPos: Int? = nil) { + client.setMarkedText(text, selectionRange: NSMakeRange(cursorPos ?? text.length, 0), replacementRange: notFoundRange) + } + + func updateCandidates(_ sugs: [String]) { + Log.debug(sugs) + // Remove duplicates + // For some weird reason, when there are duplicates, + // candidate window makes them hidden + candidates = NSOrderedSet(array: sugs).array as! [String] + updateLookupTable() + } + + func updateLookupTable() { + tableCursorPos = 0 + candidatesWindow.update() + candidatesWindow.show() } - func showActive(clientText: NSAttributedString, candidateText: String, replacementRange: NSRange? = nil) { - Logger.log.debug("Showing clientText: \(clientText) and candidateText: \(candidateText)") - client.setMarkedText(clientText, selectionRange: NSMakeRange(markedCursorLocation ?? clientText.length, 0), replacementRange: replacementRange ?? notFoundRange) - candidates = [candidateText] - if clientText.string.isEmpty { - candidatesWindow.hide() + // For moving between items of candidate table + func tableMoveEvent(_ event: NSEvent) { + if event.keyCode == kVK_UpArrow && tableCursorPos > 0 { + // TODO allow moving to the end + // This would need a custom candidate window + // https://github.com/lennylxx/google-input-tools-macos/blob/main/GoogleInputTools/CandidatesWindow.swift + tableCursorPos -= 1 + } else if event.keyCode == kVK_DownArrow && tableCursorPos < candidates.count - 1 { + tableCursorPos += 1 } - else { - candidatesWindow.update() - if config.showCandidates { - candidatesWindow.show() - } + candidatesWindow.interpretKeyEvents([event]) + } + + func getCandidate() -> String? { + if candidates.count == 0 { + return nil + } else { + return candidates[tableCursorPos] } } func finalize(_ output: String) { - Logger.log.debug("Finalizing with: \(output)") + Log.debug("Finalizing with: \(output)") client.insertText(output, replacementRange: notFoundRange) candidatesWindow.hide() - markedCursorLocation = nil } func clear() { - Logger.log.debug("Clearing MarkedText and Candidate window") + Log.debug("Clearing MarkedText and Candidate window") client.setMarkedText("", selectionRange: NSMakeRange(0, 0), replacementRange: notFoundRange) + candidates = [] candidatesWindow.hide() - markedCursorLocation = nil - } - - func findWord(at current: Int) -> NSRange? { - let maxLength = client.length() - var exponent = 2 - var wordStart = -1, wordEnd = -1 - Logger.log.debug("Finding word at: \(current) with max: \(maxLength)") - repeat { - let low = wordStart == -1 ? max(current - 2 << exponent, 0): wordStart - let high = wordEnd == -1 ? min(current + 2 << exponent, maxLength): wordEnd - Logger.log.debug("Looking for word between \(low) and \(high)") - var real = NSRange() - guard let text = client.string(from: NSMakeRange(low, high - low), actualRange: &real) else { return nil } - Logger.log.debug("Looking for word in text: \(text)") - if wordStart == -1, let startOffset = text.unicodeScalars[text.unicodeScalars.startIndex.. Bool { - if let text = transliterator.reset() { - Logger.log.debug("Committing with text: \(text)") - clientManager.finalize(text.finalaizedOutput + text.unfinalaizedOutput) - return true + public override init!(server: IMKServer, delegate: Any!, client inputClient: Any) { + guard let client = inputClient as? IMKTextInput & NSObjectProtocol else { + Log.warning("Client does not conform to the necessary protocols - refusing to initiate VarnamController!") + return nil } - else { - Logger.log.debug("Nothing to commit") - clientManager.clear() - return false + guard let clientManager = ClientManager(client: client) else { + Log.warning("Client manager failed to initialize - refusing to initiate VarnamController!") + return nil } - } + self.clientManager = clientManager + super.init(server: server, delegate: delegate, client: inputClient) - private func showActive(_ literated: Literated, replacementRange: NSRange? = nil) { - if config.outputInClient { - let attributes = mark(forStyle: kTSMHiliteConvertedText, at: replacementRange ?? client().selectedRange()) as! [NSAttributedString.Key : Any] - let clientText = NSMutableAttributedString(string: literated.finalaizedOutput + literated.unfinalaizedOutput) - clientText.addAttributes(attributes, range: NSMakeRange(0, clientText.length)) - clientManager.showActive(clientText: clientText, candidateText: literated.finalaizedInput + literated.unfinalaizedInput, replacementRange: replacementRange) - } - else { - let attributes = mark(forStyle: kTSMHiliteSelectedRawText, at: replacementRange ?? client().selectedRange()) as! [NSAttributedString.Key : Any] - let clientText = NSMutableAttributedString(string: literated.finalaizedInput + literated.unfinalaizedInput) - clientText.addAttributes(attributes, range: NSMakeRange(0, clientText.length)) - clientManager.showActive(clientText: clientText, candidateText: literated.finalaizedOutput + literated.unfinalaizedOutput, replacementRange: replacementRange) - } + initVarnam() + print("Initialized Controller for Client: \(clientManager)") } - private func moveCursorWithinMarkedText(delta: Int) -> Bool { - if transliterator.isEmpty() { - Logger.log.debug("Transliterator is empty, not handling cursor move") - } - else if !config.outputInClient, clientManager.updateMarkedCursorLocation(delta) { - showActive(transliterator.transliterate()) - return true - } - else { - commit() - } - return false + func clearState() { + preedit = "" + cursorPos = 0 + clientManager.clear() } - private func convertWord(at location: Int) { - if let wordRange = self.clientManager.findWord(at: location), wordRange.length > 0 { - var actual = NSRange() - guard let word = self.client().string(from: wordRange, actualRange: &actual), !word.isEmpty else { - return - } - Logger.log.debug("Found word: \(word) at: \(location) with actual: \(actual)") - let inputs = self.anteliterator.anteliterate(word) - Logger.log.debug("Anteliterated inputs: \(inputs)") - let literated = self.transliterator.transliterate(inputs) - if word != literated.finalaizedOutput + literated.unfinalaizedOutput { - Logger.log.error("Original: \(word) != Ante + Transliterated: \(literated.finalaizedOutput + literated.unfinalaizedOutput) - aborting conversion!") - return - } - // Calculate the location of cursor within Marked Text - self.clientManager.markedCursorLocation = config.outputInClient ? location - actual.location : transliterator.convertPosition(position: location - actual.location, fromUnits: .outputScalar, toUnits: .input) - Logger.log.debug("Marked Cursor Location: \(self.clientManager.markedCursorLocation!) for Global Location: \(location - actual.location)") - self.showActive(literated, replacementRange: actual) - } - else { - Logger.log.debug("No word found at: \(location)") - } + func commitText(_ text: String) { + clientManager.finalize(text) + clearState() } - private func dispatchConversion() { - // Don't dispatch if active session or selection is in progress - if !transliterator.isEmpty() || client().selectedRange().length != 0 { return } - // Do this asynch after 10ms to give didCommand time to return and for the client to react to the command such as moving the cursor to the new location - dispatch.schedule(deadline: .now() + .milliseconds(10)) { - [unowned self] in - if self.transliterator.isEmpty() { - self.convertWord(at: self.client().selectedRange().location) - } + // Commits the first candidate if available + func commit() { + if let text = clientManager.getCandidate() { + commitText(text) } } - public override init!(server: IMKServer, delegate: Any!, client inputClient: Any) { - guard let client = inputClient as? IMKTextInput & NSObjectProtocol else { - Logger.log.warning("Client does not conform to the necessary protocols - refusing to initiate VarnamController!") - return nil - } - guard let clientManager = ClientManager(client: client) else { - Logger.log.warning("Client manager failed to initialize - refusing to initiate VarnamController!") - return nil - } - self.clientManager = clientManager - super.init(server: server, delegate: delegate, client: inputClient) - // Initialize Literators - refreshLiterators() - Logger.log.debug("Initialized Controller for Client: \(clientManager)") + private func insertAtIndex(_ source: inout String, _ location: String.IndexDistance, _ char: String!) { + let index = source.index(source.startIndex, offsetBy: location) + source.insert(Character(char), at: index) } - public override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { - Logger.log.debug("Handling event: \(event!) from sender: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") - if event.type == .keyDown, let chars = event.characters, chars.unicodeScalars.count == 1, event.modifierFlags.isSubset(of: [.capsLock, .shift]), VarnamController.validInputs.contains(chars.unicodeScalars.first!) { - return processInput(chars, client: sender) - } - else { - return processEvent(event, client: sender) + private func removeAtIndex(_ source: inout String, _ position: String.IndexDistance) { + if let index = source.index(source.startIndex, offsetBy: position, limitedBy: source.endIndex) { + source.remove(at: index) + } else { + // out of range } } - public func processInput(_ input: String!, client sender: Any!) -> Bool { - Logger.log.debug("Processing Input: \(input!) from sender: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") - if input.unicodeScalars.count != 1 || CharacterSet.whitespaces.contains(input.unicodeScalars.first!) { - // Handle inputting of whitespace inbetween Marked Text - if let markedLocation = clientManager.markedCursorLocation { - Logger.log.debug("Handling whitespace being inserted inbetween Marked Text at: \(markedLocation)") - let literated = transliterator.transliterate() - let aggregateInputs = literated.finalaizedInput + literated.unfinalaizedInput - let committedIndex = aggregateInputs.index(aggregateInputs.startIndex, offsetBy: markedLocation) - _ = transliterator.reset() - _ = transliterator.transliterate(String(aggregateInputs.prefix(upTo: committedIndex))) - commit() - clientManager.finalize(input) - clientManager.markedCursorLocation = 0 - showActive(transliterator.transliterate(String(aggregateInputs.suffix(from: committedIndex)))) - return true + // Handle events + public override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { + if event.type != NSEvent.EventType.keyDown { + return false + } + + let keyCode = Int(event.keyCode) + + switch keyCode { + case kVK_Space: + let text = clientManager.getCandidate() + if text == nil { + commitText(preedit + " ") + } else { + commitText(text! + " ") } - else { - Logger.log.debug("Input triggered a commit; not handling the input") - commit() + return true + case kVK_Return: + let text = clientManager.getCandidate() + if text == nil { + commitText(preedit) return false + } else { + commitText(text!) } - } - if config.activeSessionOnInsert, transliterator.isEmpty() { - convertWord(at: client().selectedRange().location) - } - let literated = transliterator.transliterate(input, position: clientManager.markedCursorLocation) - if clientManager.markedCursorLocation != nil { - _ = clientManager.updateMarkedCursorLocation(1) - } - showActive(literated) - return true - } - - public func processEvent(_ event: NSEvent, client sender: Any!) -> Bool { - Logger.log.debug("Processing event: \(event) from sender: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") - // Perform shortcut actions on the system trey menu if any - if (NSApp.delegate as! AppDelegate).systemTrayMenu!.performKeyEquivalent(with: event) { return true } - // Move the cursor back to the oldLocation because commit() will move it to the end of the committed string - let oldLocation = client().selectedRange().location - Logger.log.debug("Switching \(event) at location: \(oldLocation)") - if event.modifierFlags.isEmpty && event.keyCode == kVK_Delete { // backspace - if let result = transliterator.delete(position: clientManager.markedCursorLocation) { - Logger.log.debug("Resulted in an actual delete") - if clientManager.markedCursorLocation != nil { - _ = clientManager.updateMarkedCursorLocation(-1) - } - showActive(result) - return true + return true + case kVK_Escape: + if preedit.isEmpty { + return false } - Logger.log.debug("Nothing to delete") - if commit() { - clientManager.setGlobalCursorLocation(oldLocation) + commitText(preedit) + return true + case kVK_LeftArrow: + if preedit.isEmpty { + return false } - if config.activeSessionOnDelete { - dispatchConversion() + if cursorPos > 0 { + cursorPos -= 1 + updatePreedit() } - return false - } - if event.modifierFlags.isEmpty && event.keyCode == kVK_Escape { // escape - let result = transliterator.reset() - clientManager.clear() - Logger.log.debug("Handled the cancel: \(result != nil)") - return result != nil - } - if event.modifierFlags.isEmpty && event.keyCode == kVK_Return { // return - return commit() // Don't dispatchConversion - } - if event.modifierFlags == [.numericPad, .function] && (event.keyCode == kVK_LeftArrow || event.keyCode == kVK_RightArrow) { // left or right arrow - if moveCursorWithinMarkedText(delta: event.keyCode == kVK_LeftArrow ? -1 : 1) { - return true + return true + case kVK_RightArrow: + if preedit.isEmpty { + return false } - commit() - if event.keyCode == kVK_LeftArrow { - clientManager.setGlobalCursorLocation(oldLocation) + if cursorPos < preedit.count { + cursorPos += 1 + updatePreedit() + } + return true + case kVK_UpArrow, kVK_DownArrow: + if preedit.isEmpty { + return false + } + clientManager.tableMoveEvent(event) + return true + case kVK_Delete: + if preedit.isEmpty { + return false + } + if (cursorPos > 0) { + cursorPos -= 1 + removeAtIndex(&preedit, cursorPos) + updatePreedit() + updateLookupTable() + if preedit.isEmpty { + /* Current backspace has cleared the preedit. Need to reset the engine state */ + clearState() + } + } + return true + case kVK_ForwardDelete: + if preedit.isEmpty { + return false + } + if cursorPos < preedit.count { + removeAtIndex(&preedit, cursorPos) + updatePreedit() + updateLookupTable() + if preedit.isEmpty { + /* Current delete has cleared the preedit. Need to reset the engine state */ + clearState() + } + } + return true + default: + if let chars = event.characters, chars.unicodeScalars.count == 1, event.modifierFlags.isSubset(of: [.capsLock, .shift]), VarnamController.validInputs.contains(chars.unicodeScalars.first!) { + NSLog("character event: \(chars)") + return processInput(chars) } - } - Logger.log.debug("Not processing event: \(event)") - commit() - if config.activeSessionOnCursorMove { - dispatchConversion() } return false } + public func processInput(_ input: String!) -> Bool { + insertAtIndex(&preedit, cursorPos, input) + cursorPos += 1 + updatePreedit() + + // Naming to be consistent with govarnam-ibus + updateLookupTable() + + return true + } + + private func updatePreedit() { + let attributes = mark(forStyle: kTSMHiliteSelectedRawText, at: client().selectedRange()) as! [NSAttributedString.Key : Any] + let clientText = NSMutableAttributedString(string: preedit) + clientText.addAttributes(attributes, range: NSMakeRange(0, clientText.length)) + clientManager.updatePreedit(clientText, cursorPos) + } + + private func updateLookupTable() { + let sugs = varnam.transliterate(preedit) + clientManager.updateCandidates(sugs) + } + /// This message is sent when our client looses focus public override func deactivateServer(_ sender: Any!) { - Logger.log.debug("Client: \(clientManager) loosing focus by: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") + print("Client: \(clientManager) loosing focus by: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") // Do this in case the application is quitting, otherwise we will end up with a SIGSEGV dispatch.cancelAll() - commit() + clearState() } /// This message is sent when our client gains focus public override func activateServer(_ sender: Any!) { - Logger.log.debug("Client: \(clientManager) gained focus by: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") + print("Client: \(clientManager) gained focus by: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") // There are three sources for current script selection - (a) self.currentScriptName, (b) config.scriptName and (c) selectedMenuItem.title // (b) could have changed while we were in background - converge (a) -> (b) if global script selection is configured if config.globalScriptSelection, currentScriptName != config.scriptName { - Logger.log.debug("Refreshing Literators from: \(currentScriptName) to: \(config.scriptName)") - refreshLiterators() + print("Refreshing Literators from: \(currentScriptName) to: \(config.scriptName)") +// refreshLiterators() aka initVarnam() be called again ? } } public override func menu() -> NSMenu! { - Logger.log.debug("Returning menu") + print("Returning menu") // Set the system trey menu selection to reflect our literators; converge (c) -> (a) let systemTrayMenu = (NSApp.delegate as! AppDelegate).systemTrayMenu! systemTrayMenu.items.forEach() { $0.state = .off } @@ -247,26 +231,26 @@ public class VarnamController: IMKInputController { } public override func candidates(_ sender: Any!) -> [Any]! { - Logger.log.debug("Returning Candidates for sender: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") + print("Returning Candidates for sender: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") return clientManager.candidates } public override func candidateSelected(_ candidateString: NSAttributedString!) { - Logger.log.debug("Candidate selected: \(candidateString!)") - commit() + print("Candidate selected: \(candidateString!)") + commitText(candidateString.string) } public override func commitComposition(_ sender: Any!) { - Logger.log.debug("Commit Composition called by: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") + print("Commit Composition called by: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") commit() } @objc public func menuItemSelected(sender: NSDictionary) { let item = sender.value(forKey: kIMKCommandMenuItemName) as! NSMenuItem - Logger.log.debug("Menu Item Selected: \(item.title)") + print("Menu Item Selected: \(item.title)") // Converge (b) -> (c) config.scriptName = item.representedObject as! String // Converge (a) -> (b) - refreshLiterators() + initVarnam() } } diff --git a/Input Source/VarnamIME.entitlements b/Input Source/VarnamIME.entitlements index 758c5a5..6420982 100644 --- a/Input Source/VarnamIME.entitlements +++ b/Input Source/VarnamIME.entitlements @@ -2,13 +2,15 @@ - com.apple.security.app-sandbox - - com.apple.security.temporary-exception.mach-register.global-name - VarnamController_Connection - com.apple.security.application-groups + com.apple.security.app-sandbox + + com.apple.security.temporary-exception.mach-register.global-name + VarnamController_Connection + com.apple.security.application-groups group.varnamproject.Varnam + com.apple.security.cs.allow-unsigned-executable-memory + diff --git a/Installation/build b/Installation/build index 5ab8f95..a52b51c 100755 --- a/Installation/build +++ b/Installation/build @@ -67,34 +67,78 @@ notarizefile() { # $1: path to file to notarize, $2: identifier xcrun stapler staple "$filepath" } -rm -rf ./VarnamIME.app -rm -rf ./VarnamIME.zip -rm -rf ./VarnamApp.app -rm -rf ./VarnamApp.zip -rm -rf ./VarnamIME.pkg -rm ./Scripts/installer -if [[ "$1" == "clean" ]]; then -exit 0 +clean() { + rm -rf ./VarnamIME.app + rm -rf ./VarnamIME.zip + rm -rf ./VarnamApp.app + rm -rf ./VarnamApp.zip + rm -rf ./VarnamIME.pkg + rm ./Scripts/installer +} + +build_varnam_ime() { + rm -rf ./VarnamIME.app + xcodebuild -project ../VarnamIME.xcodeproj -scheme VarnamIME -configuration Release clean build || exit 1 + cp -R ../build/Release/VarnamIME.app . + # notarizefile "VarnamIME.app" "com.varnamproject.mac.Varnam" +} + +build_shortcut_recorder() { + xcodebuild -project ../../ShortcutRecorder/ShortcutRecorder.xcodeproj -scheme "ShortcutRecorder.framework" -configuration Release clean build || exit 1 + rm -rf ../Application/ShortcutRecorder.framework + cp -R ../../ShortcutRecorder/build/Release/ShortcutRecorder.framework ../Application/ || exit 1 +} + +build_varnam_app() { + rm -rf ./VarnamApp.app + xcodebuild -project ../VarnamIME.xcodeproj -scheme VarnamApp -configuration Release clean build || exit 1 + cp -R ../build/Release/VarnamApp.app . + # notarizefile "VarnamApp.app" "com.varnamproject.mac.VarnamApp" +} + +build_varnam_installer() { + rm ./Scripts/installer + xcodebuild -project ../VarnamIME.xcodeproj -scheme Installer -configuration Release clean build || exit 1 + cp ../build/Release/installer ./Scripts/ +} + +build_pkg() { + packagesbuild -v VarnamIME.pkgproj + # notarizefile "VarnamIME.pkg" "com.varnamproject.mac.VarnamApp" +} + +if [[ "$1" == "clean" ]]; then + clean + exit 0 +fi + +if [[ "$1" == "ime" ]]; then + rm -rf ./VarnamIME.app + rm -rf ./VarnamIME.zip + build_varnam_ime + exit 0 +fi + +if [[ "$1" == "ShortcutRecorder" ]]; then + build_shortcut_recorder + exit 0 +fi + +# Not gonna use LipikaEngine anymore. TODO remove this when it's not needed +if [ ! -d ../Input\ Source/LipikaEngine_OSX.framework ]; then + xcodebuild -project ../../lipika-engine/LipikaEngine.xcodeproj -scheme "LipikaEngine_OSX" -configuration Debug clean build test || exit 1 + xcodebuild -project ../../lipika-engine/LipikaEngine.xcodeproj -scheme "LipikaEngine_OSX" -configuration Release clean build || exit 1 + rm -rf ../Input\ Source/LipikaEngine_OSX.framework + cp -R ../../lipika-engine/build/Release/LipikaEngine_OSX.framework ../Input\ Source/ +fi + +if [ ! -d ../Application/ShortcutRecorder.framework ]; then + build_shortcut_recorder fi -xcodebuild -project ../../lipika-engine/LipikaEngine.xcodeproj -scheme "LipikaEngine_OSX" -configuration Debug clean build test || exit 1 -xcodebuild -project ../../lipika-engine/LipikaEngine.xcodeproj -scheme "LipikaEngine_OSX" -configuration Release clean build || exit 1 -rm -rf ../Input\ Source/LipikaEngine_OSX.framework -cp -R ../../lipika-engine/build/Release/LipikaEngine_OSX.framework ../Input\ Source/ -xcodebuild -project ../VarnamIME.xcodeproj -scheme VarnamIME -configuration Release clean build || exit 1 -cp -R ../build/Release/VarnamIME.app . -# notarizefile "VarnamIME.app" "com.varnamproject.mac.Varnam" - -xcodebuild -project ../../ShortcutRecorder/ShortcutRecorder.xcodeproj -scheme "ShortcutRecorder.framework" -configuration Release clean build || exit 1 -rm -rf ../Application/ShortcutRecorder.framework -cp -R ../../ShortcutRecorder/build/Release/ShortcutRecorder.framework ../Application/ || exit 1 -xcodebuild -project ../VarnamIME.xcodeproj -scheme VarnamApp -configuration Release clean build || exit 1 -cp -R ../build/Release/VarnamApp.app . -# notarizefile "VarnamApp.app" "com.varnamproject.mac.VarnamApp" - -xcodebuild -project ../VarnamIME.xcodeproj -scheme Installer -configuration Release clean build || exit 1 -cp ../build/Release/installer ./Scripts/ -packagesbuild -v VarnamIME.pkgproj -# notarizefile "VarnamIME.pkg" "com.varnamproject.mac.VarnamApp" +build_varnam_ime +build_varnam_app +build_varnam_installer +build_pkg exit 0 diff --git a/VarnamIME.xcodeproj/project.pbxproj b/VarnamIME.xcodeproj/project.pbxproj index 75ec781..4f37aed 100644 --- a/VarnamIME.xcodeproj/project.pbxproj +++ b/VarnamIME.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 04D0A680273FC2BC006C3B54 /* libgovarnam.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */; }; + 04D0A681273FC2C0006C3B54 /* libgovarnam.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 04D0A683273FC367006C3B54 /* Varnam.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D0A682273FC367006C3B54 /* Varnam.swift */; }; A5122A4E20D42A2000575848 /* InputSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53AD01A20CC658000C95844 /* InputSource.swift */; }; A5122A4F20D42A2000575848 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5122A4220D420E100575848 /* main.swift */; }; A5122A5020D42A9500575848 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A546182A20CC6BF3003A73EB /* InputMethodKit.framework */; }; @@ -64,6 +67,7 @@ dstSubfolderSpec = 10; files = ( A53AD01220CC643700C95844 /* LipikaEngine_OSX.framework in Embed Frameworks */, + 04D0A681273FC2C0006C3B54 /* libgovarnam.dylib in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -83,6 +87,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 04D0A67B273FC20D006C3B54 /* VarnamIME-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VarnamIME-Bridging-Header.h"; sourceTree = ""; }; + 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libgovarnam.dylib; sourceTree = ""; }; + 04D0A682273FC367006C3B54 /* Varnam.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Varnam.swift; sourceTree = ""; }; A5122A4220D420E100575848 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; A5122A4720D429D300575848 /* Installer */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = Installer; sourceTree = BUILT_PRODUCTS_DIR; }; A52905DC234EC67900A8D95E /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; @@ -140,6 +147,7 @@ buildActionMask = 2147483647; files = ( A546182920CC6BEA003A73EB /* Cocoa.framework in Frameworks */, + 04D0A680273FC2BC006C3B54 /* libgovarnam.dylib in Frameworks */, A546182B20CC6BF3003A73EB /* InputMethodKit.framework in Frameworks */, A53AD01520CC64FD00C95844 /* LipikaEngine_OSX.framework in Frameworks */, ); @@ -158,9 +166,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 04D0A67A273FC1F1006C3B54 /* GoVarnam */ = { + isa = PBXGroup; + children = ( + 04D0A682273FC367006C3B54 /* Varnam.swift */, + 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */, + 04D0A67B273FC20D006C3B54 /* VarnamIME-Bridging-Header.h */, + ); + path = GoVarnam; + sourceTree = ""; + }; A53ACFF420CC347900C95844 = { isa = PBXGroup; children = ( + 04D0A67A273FC1F1006C3B54 /* GoVarnam */, A546181D20CC69EC003A73EB /* Documents */, A53ACFFF20CC347900C95844 /* Input Source */, A57F18EA20D1FBA1007C9745 /* Installation */, @@ -341,7 +360,7 @@ }; A53ACFFC20CC347900C95844 = { CreatedOnToolsVersion = 9.4; - LastSwiftMigration = 1030; + LastSwiftMigration = 1250; SystemCapabilities = { com.apple.ApplicationGroups.Mac = { enabled = 1; @@ -434,6 +453,7 @@ files = ( A53AD00120CC347900C95844 /* AppDelegate.swift in Sources */, A53AD01F20CC660900C95844 /* VarnamController.swift in Sources */, + 04D0A683273FC367006C3B54 /* Varnam.swift in Sources */, A53AD01720CC654700C95844 /* VarnamConfig.swift in Sources */, A53AD01D20CC659C00C95844 /* ClientManager.swift in Sources */, A55381F521D703EA00A3CD8A /* MappingStore.swift in Sources */, @@ -641,6 +661,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "Input Source/VarnamIME.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; @@ -659,11 +680,18 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/GoVarnam", + ); MARKETING_VERSION = 2.2; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.varnamproject.mac.Varnam; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "GoVarnam/VarnamIME-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -672,6 +700,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "Input Source/VarnamIME.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; @@ -690,11 +719,17 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/GoVarnam", + ); MARKETING_VERSION = 2.2; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.varnamproject.mac.inputmethod.Varnam; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "GoVarnam/VarnamIME-Bridging-Header.h"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release;