├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── Packages ├── ilimi_MainAssembly │ ├── .gitignore │ ├── .swiftlint.yml │ ├── Package.swift │ ├── Sources │ │ ├── IMKCandidatesImpl │ │ │ ├── IMKCandidatesImpl.m │ │ │ └── include │ │ │ │ └── IMKCandidatesImpl.h │ │ ├── IOKitCHeaders │ │ │ ├── CapsLockToggler.c │ │ │ └── include │ │ │ │ └── CapsLockToggler.h │ │ └── ilimiMainAssembly │ │ │ ├── AppDelegate.swift │ │ │ ├── Extension │ │ │ ├── Binding.swift │ │ │ ├── CharWidthConverter.swift │ │ │ └── Nsevent.swift │ │ │ ├── IlimiControllerEventHandler.swift │ │ │ ├── IlimiInputController.swift │ │ │ ├── LangModelAssembly │ │ │ ├── DataInitilizer.swift │ │ │ ├── DataModel │ │ │ │ ├── CoreDataHelper.swift │ │ │ │ └── PersistenceController.swift │ │ │ └── WordDictReader │ │ │ │ ├── CinReader.swift │ │ │ │ └── LiuUniTabConverter.swift │ │ │ ├── Resources │ │ │ └── Model.xcdatamodeld │ │ │ │ └── Model.xcdatamodel │ │ │ │ └── contents │ │ │ ├── Utils │ │ │ ├── AutoCheckUpdate │ │ │ │ ├── Model │ │ │ │ │ ├── CustomError.swift │ │ │ │ │ └── GithubRelease.swift │ │ │ │ └── UpdateManager.swift │ │ │ ├── CustomPhrase │ │ │ │ └── CustomPhraseManager.swift │ │ │ ├── FullWidthMode │ │ │ │ └── FullWidthMode.swift │ │ │ ├── InputEngine │ │ │ │ ├── InputContext.swift │ │ │ │ └── InputEngine.swift │ │ │ ├── KeyEvent │ │ │ │ ├── CapsLockToggler.swift │ │ │ │ └── NSEventImpl.swift │ │ │ ├── NormalMode │ │ │ │ └── LiuManager.swift │ │ │ ├── Notification │ │ │ │ ├── Beep.swift │ │ │ │ └── Notification.swift │ │ │ ├── SamePronunciationMode │ │ │ │ └── SamePronunciationMode.swift │ │ │ ├── SpMode │ │ │ │ └── SpModeManager.swift │ │ │ ├── StringConvert │ │ │ │ └── StringConverter.swift │ │ │ └── ZhuyinMode │ │ │ │ └── ZhuyinMode.swift │ │ │ ├── View │ │ │ ├── AddCustomPhrase │ │ │ │ ├── AddCustomPhraseSheetView.swift │ │ │ │ └── AddCustomPhraseView.swift │ │ │ ├── Menu │ │ │ │ ├── IlimiMenu.swift │ │ │ │ └── MainMenu.swift │ │ │ ├── Notifier │ │ │ │ └── Notifier.swift │ │ │ ├── Query │ │ │ │ └── QueryView.swift │ │ │ └── Settings │ │ │ │ ├── GeneralSettingsView.swift │ │ │ │ └── SettingsViewToolbar.swift │ │ │ └── ViewModel │ │ │ ├── CustomPhraseViewModel.swift │ │ │ └── SettingViewModel.swift │ └── Tests │ │ └── ilimiMainAssemblyTests │ │ ├── ComponentsForTests │ │ └── vChewing │ │ │ ├── KeyCodeMapForTests.swift │ │ │ ├── MockedClient.swift │ │ │ └── NSEventImplForTests.swift │ │ └── ilimiMainAssemblyTests.swift └── vChewing_IMKUtils_IlimiImpl │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources │ └── IMKUtils │ │ ├── IMKHelper.swift │ │ ├── LatinKeyboardMappings.swift │ │ └── TISInputSourceExtension.swift │ └── Tests │ └── IMKUtilsTests │ └── IMKUtilsTests.swift ├── README.md ├── build.sh ├── ilimi.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── chenli.xcuserdatad │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ └── ilimi.xcscheme └── xcuserdata │ └── chenli.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── ilimi ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ └── Contents.json ├── IlimiKeyboard.keylayout ├── Info.plist ├── MenuIcons │ ├── MenuIcon-ILIMI.png │ └── MenuIcon-ILIMI@2x.png ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── en.lproj │ └── InfoPlist.strings ├── ilimi.entitlements ├── main.swift └── zh-Hant.lproj │ └── InfoPlist.strings ├── ilimiInstaller ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── Contents.json │ └── IconSansMargin.imageset │ │ ├── Contents.json │ │ └── IconSansMargin.heic ├── Installer-Info.plist ├── InstallerShared.swift ├── MainView.swift ├── MainViewImpl.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── RelocationDetector.swift ├── en.lproj │ ├── InfoPlist.strings │ ├── Installer-Info.plist │ └── Localizable.strings ├── ilimiInstaller.entitlements ├── ilimiInstallerApp.swift └── zh-Hant.lproj │ ├── InfoPlist.strings │ ├── Installer-Info.plist │ └── Localizable.strings ├── media ├── ascii_demo.gif ├── custom_phrase_demo.png ├── demo01.gif ├── demo02.gif ├── demo03.gif ├── demo04.gif └── zhuyin_demo.gif ├── others ├── image_assets │ ├── AppIcon-ilimi-RAW.heic │ └── AppIcon-ilimi.pxd ├── pinyin.json ├── pinyin.txt └── pinyin_txt_to_json.py └── pinyin.bundle └── pinyin.json /.gitignore: -------------------------------------------------------------------------------- 1 | !**/[Pp]ackages/build/ 2 | !*.[Cc]ache/ 3 | !.axoCover/settings.json 4 | $RECYCLE.BIN/ 5 | $tf/ 6 | **/*.DesktopClient/GeneratedArtifacts 7 | **/*.DesktopClient/ModelManifest.xml 8 | **/*.HTMLClient/GeneratedArtifacts 9 | **/*.Server/GeneratedArtifacts 10 | **/*.Server/ModelManifest.xml 11 | *.app 12 | *.appx 13 | *.aps 14 | *.azurePubxml 15 | *.bim.layout 16 | *.bim_*.settings 17 | *.binlog 18 | *.btm.cs 19 | *.btp.cs 20 | *.build.csdef 21 | *.cab 22 | *.cachefile 23 | *.coverage 24 | *.coveragexml 25 | *.dbmdl 26 | *.dbproj.schemaview 27 | *.dmg 28 | *.dotCover 29 | *.DotSettings.user 30 | *.e2e 31 | *.GhostDoc.xml 32 | *.gpState 33 | *.ilk 34 | *.iobj 35 | *.ipdb 36 | *.jfm 37 | *.jmconfig 38 | *.ldf 39 | *.lnk 40 | *.log 41 | *.mdf 42 | *.meta 43 | *.mm.* 44 | *.mode1v3 45 | *.msi 46 | *.msix 47 | *.msm 48 | *.msp 49 | *.ncb 50 | *.ndf 51 | *.nuget.props 52 | *.nuget.targets 53 | *.nupkg 54 | *.nvuser 55 | *.obj 56 | *.odx.cs 57 | *.opendb 58 | *.opensdf 59 | *.opt 60 | *.pbxuser 61 | *.pch 62 | *.pdb 63 | *.pfx 64 | *.pgc 65 | *.pgd 66 | *.pidb 67 | *.plg 68 | *.psess 69 | *.publishproj 70 | *.publishsettings 71 | *.pubxml 72 | *.pyc 73 | *.rdl.data 74 | *.rptproj.bak 75 | *.rptproj.rsuser 76 | *.rsp 77 | *.sap 78 | *.sbr 79 | *.scc 80 | *.sdf 81 | *.sln.docstates 82 | *.sln.iml 83 | *.stackdump 84 | *.suo 85 | *.svclog 86 | *.tlb 87 | *.tlh 88 | *.tli 89 | *.tmp 90 | *.tmp_proj 91 | *.tm_build_errors 92 | *.tss 93 | *.user 94 | *.userosscache 95 | *.userprefs 96 | *.usertasks 97 | *.vbw 98 | *.VC.db 99 | *.VC.VC.opendb 100 | *.VisualState.xml 101 | *.vsp 102 | *.vspscc 103 | *.vspx 104 | *.vssscc 105 | *.xsd.cs 106 | *.[Cc]ache 107 | *.[Pp]ublish.xml 108 | *.[Rr]e[Ss]harper 109 | *_h.h 110 | *_i.c 111 | *_p.c 112 | *_wpftmp.csproj 113 | *~ 114 | .*crunch*.local.xml 115 | .apdisk 116 | .AppleDB 117 | .AppleDesktop 118 | .AppleDouble 119 | .axoCover/* 120 | .build 121 | .builds 122 | .com.apple.timemachine.donotpresent 123 | .cr/personal 124 | .DocumentRevisions-V100 125 | .DS_Store 126 | .fake/ 127 | .fseventsd 128 | .idea 129 | .idea/ 130 | .JustCode 131 | .localhistory/ 132 | .LSOverride 133 | .mfractor/ 134 | .ntvs_analysis.dat 135 | .paket/paket.exe 136 | .sass-cache/ 137 | .Spotlight-V100 138 | .swiftpm 139 | .TemporaryItems 140 | .Trashes 141 | .VolumeIcon.icns 142 | .vs/ 143 | .vscode 144 | ._* 145 | aclocal.m4 146 | AppPackages/ 147 | artifacts/ 148 | ASALocalRun/ 149 | autom4te.cache/ 150 | AutoTest.Net/ 151 | Backup*/ 152 | BenchmarkDotNet.Artifacts/ 153 | bld/ 154 | build 155 | BundleArtifacts/ 156 | ClientBin/ 157 | config.make 158 | config.status 159 | Credits.rtf 160 | csx/ 161 | dlldata.c 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/*.HxC 167 | DocProject/Help/*.HxT 168 | DocProject/Help/html 169 | DocProject/Help/Html2 170 | ecf/ 171 | ehthumbs.db 172 | ehthumbs_vista.db 173 | FakesAssemblies/ 174 | Generated\ Files/ 175 | Generated_Code/ 176 | Icon 177 | Installer/PKGRoot/ 178 | install-sh 179 | ipch/ 180 | Makefile.in 181 | nCrunchTemp_* 182 | Network Trash Folder 183 | node_modules/ 184 | OpenCover/ 185 | orleans.codegen.cs 186 | Package.StoreAssociation.xml 187 | paket-files/ 188 | project.fragment.lock.json 189 | project.lock.json 190 | project.xcworkspace 191 | publish/ 192 | PublishScripts/ 193 | rcf/ 194 | ServiceFabricBackup/ 195 | Source/Data/* 196 | StyleCopReport.xml 197 | tarballs/ 198 | Temporary Items 199 | test-results/ 200 | TestResult.xml 201 | Thumbs.db 202 | UpgradeLog*.htm 203 | UpgradeLog*.XML 204 | x64/ 205 | x86/ 206 | xcuserdata 207 | [Bb]in/ 208 | [Bb]uild 209 | [Bb]uild[Ll]og.* 210 | [Dd]ebug/ 211 | [Dd]ebugPS/ 212 | [Dd]ebugPublic/ 213 | [Dd]esktop.ini 214 | [Ee]xpress/ 215 | [Ll]og/ 216 | [Oo]bj/ 217 | [Rr]elease/ 218 | [Rr]eleasePS/ 219 | [Rr]eleases/ 220 | [Tt]est[Rr]esult*/ 221 | _Chutzpah* 222 | _NCrunch_* 223 | _pkginfo.txt 224 | _Pvt_Extensions 225 | _ReSharper*/ 226 | _TeamCity* 227 | _UpgradeReport_Files/ 228 | __pycache__/ 229 | ~$* 230 | DataCompiler/dataCompiler.exe 231 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # SwiftFormat config compliant with Google Swift Guideline 2 | # https://google.github.io/swift/#control-flow-statements 3 | 4 | # Specify version used in a project 5 | 6 | --swiftversion 5.7 7 | 8 | # Rules explicitly required by the guideline 9 | 10 | --rules \ 11 | blankLinesAroundMark, \ 12 | blankLinesAtEndOfScope, \ 13 | blankLinesAtStartOfScope, \ 14 | blankLinesBetweenScopes, \ 15 | braces, \ 16 | consecutiveBlankLines, \ 17 | consecutiveSpaces, \ 18 | duplicateImports, \ 19 | elseOnSameLine, \ 20 | emptyBraces, \ 21 | enumNamespaces, \ 22 | extensionAccessControl, \ 23 | hoistPatternLet, \ 24 | indent, \ 25 | leadingDelimiters, \ 26 | linebreakAtEndOfFile, \ 27 | markTypes, \ 28 | organizeDeclarations, \ 29 | redundantInit, \ 30 | redundantParens, \ 31 | redundantPattern, \ 32 | redundantRawValues, \ 33 | redundantType, \ 34 | redundantVoidReturnType, \ 35 | semicolons, \ 36 | sortImports, \ 37 | sortSwitchCases, \ 38 | spaceAroundBraces, \ 39 | spaceAroundBrackets, \ 40 | spaceAroundComments, \ 41 | spaceAroundGenerics, \ 42 | spaceAroundOperators, \ 43 | spaceAroundParens, \ 44 | spaceInsideBraces, \ 45 | spaceInsideBrackets, \ 46 | spaceInsideComments, \ 47 | spaceInsideGenerics, \ 48 | spaceInsideParens, \ 49 | todos, \ 50 | trailingClosures, \ 51 | trailingCommas, \ 52 | trailingSpace, \ 53 | typeSugar, \ 54 | void, \ 55 | wrap, \ 56 | wrapArguments, \ 57 | wrapAttributes, \ 58 | # 59 | # 60 | # Additional rules not mentioned in the guideline, but helping to keep the codebase clean 61 | # Quoting the guideline: 62 | # Common themes among the rules in this section are: 63 | # avoid redundancy, avoid ambiguity, and prefer implicitness over explicitness 64 | # unless being explicit improves readability and/or reduces ambiguity. 65 | # 66 | # 67 | andOperator, \ 68 | isEmpty, \ 69 | redundantBackticks, \ 70 | redundantBreak, \ 71 | redundantExtensionACL, \ 72 | redundantGet, \ 73 | redundantLetError, \ 74 | redundantNilInit, \ 75 | redundantObjc, \ 76 | redundantReturn, \ 77 | redundantSelf, \ 78 | strongifiedSelf 79 | 80 | 81 | # Options for basic rules 82 | 83 | --extensionacl on-declarations 84 | --funcattributes prev-line 85 | --indent 4 86 | --maxwidth 120 87 | --typeattributes prev-line 88 | --varattributes same-line 89 | --voidtype void 90 | --wraparguments before-first 91 | --wrapparameters before-first 92 | --wrapcollections before-first 93 | --wrapreturntype if-multiline 94 | --wrapconditions after-first 95 | 96 | # Option for additional rules 97 | 98 | --self init-only 99 | 100 | # Excluded folders 101 | 102 | --exclude Pods,**/UNTESTED_TODO,vendor,fastlane,./Build,*.build* 103 | 104 | # https://github.com/NoemiRozpara/Google-SwiftFormat-Config 105 | 106 | # Following is by Lava 107 | 108 | --ifdef no-indent 109 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | disabled_rules: 18 | - discouraged_optional_collection 19 | - multiple_closures_with_trailing_closure 20 | - nesting 21 | 22 | opt_in_rules: 23 | - convenience_type 24 | # - no_magic_numbers 25 | # - force_unwrapping 26 | 27 | indentation_width: 4 28 | force_cast: warning # implicitly 29 | force_try: 30 | severity: warning # explicitly 31 | line_length: 120 32 | function_body_length: 33 | warning: 120 34 | error: 400 35 | type_body_length: 36 | warning: 500 37 | error: 1200 38 | file_length: 39 | warning: 900 40 | error: 1600 41 | type_name: 42 | min_length: 3 43 | max_length: 44 | warning: 50 45 | error: 50 46 | excluded: 47 | - OS 48 | identifier_name: 49 | min_length: 3 50 | excluded: # excluded via string array 51 | - id 52 | - URL 53 | - url 54 | - x 55 | - y 56 | - i 57 | - j 58 | - OS 59 | - Defaults # Make use of `SwiftyUserDefaults` 60 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji) 61 | trailing_comma: 62 | severity: warning 63 | mandatory_comma: true 64 | # force_unwrapping: 65 | # excluded: 66 | # - ".*Test\\.swift" 67 | 68 | custom_rules: 69 | sf_safe_symbol: 70 | name: "Safe SFSymbol" 71 | message: "Use `SFSafeSymbols` via `systemSymbol` parameters for type safety." 72 | regex: "(Image\\(systemName:)|(NSImage\\(symbolName:)|(Label[^,]+?,\\s*systemImage:)|(UIApplicationShortcutIcon\\(systemImageName:)" 73 | severity: warning 74 | 75 | excluded: 76 | - "Build/*" 77 | - "*/.build/*" 78 | - "*/resource_bundle_accessor.swift" 79 | - "resource_bundle_accessor.swift" 80 | - Pods 81 | - R.generated.swift 82 | - .build # Where Swift Package Manager checks out dependency sources 83 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 自v1.0.2開始新增changelog 2 | 3 | ## v1.0.2 4 | 5 | 1. [修復] 修復修正輸入「,,ct」時不能啟用「打繁出簡」的故障。 6 | 2. [修復] 沒有匯入「pinyin.json」的使用者在使用注音模式時,如今會顯示提示。 7 | 8 | ## v1.1 9 | 10 | 從這一版本開始,威注音專案參與對一粒米輸入法的協力工作。 11 | 12 | 1. [修復] 修復了 App 圖示缺失、輸入法選單圖示缺失的故障。 13 | 2. [修復] 補齊了輸入法選單當中的本地化名稱。 14 | 3. [修復] 修正了 Alacrity 終端機內藉由輸入法自身的 CapsLock 模式無法敲小寫的故障。 15 | - 原始描述:修正在alacritty中,使用英數模式的情況即使沒按下shift也會因capslock啟動而輸出大寫字母。這個情況只有使用全英鍵盤佈局時會發生,解方是使用客製化鍵盤佈局。 16 | 4. [研發] 全專案架構改組,將輸入法本體全部塞到 SPM 內。 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/sh 2 | .PHONY: format 3 | 4 | format: 5 | @git ls-files --exclude-standard | grep -E '\.swift$$' | swiftlint --fix --autocorrect 6 | @swiftformat --swiftversion 5.7 ./ 7 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # 执行时排除掉的规则 2 | - void_return 3 | # - colon 4 | # - comma 5 | # - control_statement 6 | opt_in_rules: # 一些规则仅仅是可选的 7 | # - empty_count 8 | # - missing_docs 9 | # 可以通过执行如下指令来查找所有可用的规则: 10 | # swiftlint rules 11 | included: # 执行 linting 时包含的路径。如果出现这个 `--path` 会被忽略。 12 | # - Source 13 | excluded: # 执行 linting 时忽略的路径。 优先级比 `included` 更高。 14 | - Dependences/GachaMIMTServer 15 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ilimiMainAssembly", 7 | platforms: [ 8 | .macOS(.v13), 9 | ], 10 | products: [ 11 | // Products define the executables and libraries a package produces, making them visible to other packages. 12 | .library( 13 | name: "ilimiMainAssembly", 14 | targets: ["ilimiMainAssembly"] 15 | ), 16 | .library( 17 | name: "IMKCandidatesImpl", 18 | targets: ["IMKCandidatesImpl"] 19 | ), 20 | ], 21 | dependencies: [ 22 | .package(path: "../vChewing_IMKUtils_IlimiImpl"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package, defining a module or a test suite. 26 | // Targets can depend on other targets in this package and products from dependencies. 27 | .target( 28 | name: "ilimiMainAssembly", 29 | dependencies: [ 30 | "IOKitCHeaders", 31 | "IMKCandidatesImpl", 32 | .product(name: "vChewing_IMKUtils_IlimiImpl", package: "vChewing_IMKUtils_IlimiImpl"), 33 | ], 34 | resources: [ 35 | .process("Resources/Model.xcdatamodeld"), 36 | ] 37 | ), 38 | .target( 39 | name: "IOKitCHeaders", 40 | resources: [] 41 | ), 42 | .target( 43 | name: "IMKCandidatesImpl", 44 | resources: [] 45 | ), 46 | .testTarget( 47 | name: "ilimiMainAssemblyTests", 48 | dependencies: ["ilimiMainAssembly"] 49 | ), 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/IMKCandidatesImpl/IMKCandidatesImpl.m: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | #import 6 | #import 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | @implementation IMKCandidates (ilimi) 11 | 12 | @end 13 | 14 | NS_ASSUME_NONNULL_END 15 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/IMKCandidatesImpl/include/IMKCandidatesImpl.h: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | #ifndef ilimi_Bridging_Header_h 6 | #define ilimi_Bridging_Header_h 7 | 8 | #endif /* ilimi_Bridging_Header_h */ 9 | 10 | //#import 11 | #import 12 | 13 | @interface IMKCandidates (ilimi) { 14 | } 15 | 16 | - (unsigned long long)windowLevel; // Please do not use perform-selector with this since it will return a null value. 17 | - (void)setWindowLevel:(unsigned long long)level; // Please do not use perform-selector with this since it never works. 18 | - (void)setFontSize:(double)fontSize; 19 | @end 20 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/IOKitCHeaders/CapsLockToggler.c: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/IOKitCHeaders/include/CapsLockToggler.h: -------------------------------------------------------------------------------- 1 | // Ref: https://stackoverflow.com/a/75870807/4162914 2 | 3 | #import 4 | #import 5 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Cocoa 6 | import InputMethodKit 7 | import SwiftUI 8 | import UserNotifications 9 | 10 | public class AppDelegate: NSObject, NSApplicationDelegate { 11 | // MARK: Public 12 | 13 | public static var shared = AppDelegate() 14 | 15 | public func applicationDidFinishLaunching(_ notification: Notification) { 16 | // Insert code here to initialize your application 17 | DataInitializer.shared.initDataWhenStart() 18 | doOnFirstRun() 19 | // regist UserDefaluts 20 | registUserDefaultsSetting() 21 | // notification 22 | userNotificationCenter.delegate = self 23 | requestNotificationAuthorization() 24 | // 用程式碼方法補上MainMenu.xib 25 | NSApplication.shared.mainMenu = MainMenu() 26 | // NSLog("connection tried") 27 | } 28 | 29 | public func applicationWillTerminate(_ notification: Notification) { 30 | // Insert code here to tear down your application 31 | } 32 | 33 | // MARK: Internal 34 | 35 | var queryWindow: NSWindow? 36 | var settingsWindow: NSWindow? 37 | let userNotificationCenter = UNUserNotificationCenter.current() 38 | 39 | func doOnFirstRun() { 40 | let firstRun = UserDefaults.standard.bool(forKey: "firstRun") as Bool 41 | if !firstRun { 42 | CustomPhraseManager.setDefaultCustomPhrase() 43 | UserDefaults.standard.setValue(true, forKey: "firstRun") 44 | } 45 | } 46 | 47 | func registUserDefaultsSetting() { 48 | UserDefaults.standard.register(defaults: ["isHorizontalCandidatesPanel": true]) 49 | UserDefaults.standard.register(defaults: ["limitInputWhenNoCandidate": false]) 50 | UserDefaults.standard.register(defaults: ["showLiuKeyAfterZhuyin": true]) 51 | UserDefaults.standard.register(defaults: ["silentMode": false]) 52 | UserDefaults.standard.register(defaults: ["selectCandidateBy1to8": true]) 53 | UserDefaults.standard.register(defaults: ["autoCheckUpdate": true]) 54 | } 55 | 56 | // request the authorization for pushing local notification 57 | func requestNotificationAuthorization() { 58 | userNotificationCenter.requestAuthorization(options: [.alert, .badge]) { 59 | _, _ in 60 | } 61 | } 62 | 63 | // 設定視窗 64 | func showSettingsWindow(_ tabIndex: Int = 0) { 65 | let context = CustomPhraseManager.context 66 | if let settingsWindow = settingsWindow { 67 | if settingsWindow.isVisible { 68 | switch tabIndex { 69 | case 1: 70 | settingsWindow.contentView = NSHostingView( 71 | rootView: AddCustomPhraseView() 72 | .environment(\.managedObjectContext, context) 73 | ) 74 | default: 75 | settingsWindow.contentView = NSHostingView(rootView: GeneralSettingsView()) 76 | } 77 | settingsWindow.makeKeyAndOrderFront(self) 78 | settingsWindow.orderFrontRegardless() 79 | NSApp.activate(ignoringOtherApps: true) 80 | return 81 | } 82 | } 83 | settingsWindow = NSWindow( 84 | contentRect: NSRect(x: 0, y: 0, width: 400, height: 250), 85 | styleMask: [.closable, .resizable, .miniaturizable, .titled], 86 | backing: .buffered, 87 | defer: false 88 | ) 89 | settingsWindow?.toolbarStyle = NSWindow.ToolbarStyle.preference 90 | NSToolbar.settingsViewToolBar.delegate = self 91 | settingsWindow?.toolbar = NSToolbar.settingsViewToolBar 92 | switch tabIndex { 93 | case 1: 94 | settingsWindow?.contentView = NSHostingView( 95 | rootView: AddCustomPhraseView() 96 | .environment(\.managedObjectContext, context) 97 | ) 98 | default: 99 | settingsWindow?.contentView = NSHostingView(rootView: GeneralSettingsView()) 100 | } 101 | settingsWindow?.center() 102 | settingsWindow?.makeKeyAndOrderFront(self) 103 | settingsWindow?.orderFrontRegardless() 104 | settingsWindow?.isReleasedWhenClosed = false 105 | NSApp.activate(ignoringOtherApps: true) 106 | } 107 | 108 | // 查碼視窗 109 | func showQueryWindow() { 110 | if let queryWindow = queryWindow { 111 | if queryWindow.isVisible { 112 | queryWindow.makeKeyAndOrderFront(self) 113 | queryWindow.orderFrontRegardless() 114 | return 115 | } 116 | } 117 | queryWindow = NSWindow( 118 | contentRect: NSRect(x: 0, y: 0, width: 400, height: 300), 119 | styleMask: [.closable, .resizable, .miniaturizable, .titled], 120 | backing: .buffered, 121 | defer: false 122 | ) 123 | queryWindow?.collectionBehavior = [.stationary, .canJoinAllSpaces, .fullScreenAuxiliary] 124 | let queryView = QueryView() 125 | queryWindow?.center() 126 | queryWindow?.contentView = NSHostingView(rootView: queryView) 127 | queryWindow?.makeKeyAndOrderFront(self) 128 | queryWindow?.orderFrontRegardless() 129 | queryWindow?.isReleasedWhenClosed = false 130 | NSApp.setActivationPolicy(.accessory) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Extension/Binding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/4/9. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension Binding { 12 | func toUnwrapped(defaultValue: T) -> Binding where Value == T? { 13 | Binding(get: { self.wrappedValue ?? defaultValue }, set: { self.wrappedValue = $0 }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Extension/CharWidthConverter.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Foundation 6 | 7 | extension String { 8 | // 轉半形 9 | var halfWidth: String { 10 | transformFullWidthToHalfWidth(reverse: false) 11 | } 12 | 13 | // 轉全型 14 | var fullWidth: String { 15 | transformFullWidthToHalfWidth(reverse: true) 16 | } 17 | 18 | private func transformFullWidthToHalfWidth(reverse: Bool) -> String { 19 | let string = NSMutableString(string: self) as CFMutableString 20 | CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, reverse) 21 | return string as String 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Extension/Nsevent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/4/18. 6 | // 7 | 8 | import AppKit 9 | import Foundation 10 | 11 | extension NSEvent { 12 | var isDeleteKey: Bool { 13 | keyCode == 51 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/LangModelAssembly/DataInitilizer.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | import CoreData 7 | import Foundation 8 | 9 | // MARK: - DataInitializer 10 | 11 | class DataInitializer { 12 | static let shared = DataInitializer() 13 | static let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] 14 | .appendingPathComponent("ilimi") 15 | static let appSupportDir = appSupportURL.path 16 | 17 | let persistenceContainer = PersistenceController.shared 18 | let liuUniTab = appSupportDir + "/liu-uni.tab" 19 | let liuJsonPath = appSupportDir + "/liu.json" 20 | let liuCinPath = appSupportDir + "/liu.cin" 21 | 22 | let userDefaults = UserDefaults.standard 23 | 24 | func initDataWhenStart() { 25 | let hadReadLiu = userDefaults.object(forKey: "hadReadLiuJson") as? Bool ?? false 26 | if !hadReadLiu { 27 | loadLiuData() 28 | } 29 | let hadReadPinyin = userDefaults.object(forKey: "hadReadPinyinJson") as? Bool ?? false 30 | if !hadReadPinyin { 31 | loadPinyinJson() 32 | } 33 | } 34 | 35 | func cleanAllData(_ entityName: String) { 36 | let fetchRequest = NSFetchRequest(entityName: entityName) 37 | let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 38 | do { 39 | try persistenceContainer.container.viewContext.execute(batchDeleteRequest) 40 | } catch { 41 | AppDelegate.shared.pushInstantNotification( 42 | title: String(describing: error), 43 | subtitle: "", 44 | body: "", 45 | sound: true 46 | ) 47 | } 48 | // NSLog("Core Data cleaned") 49 | } 50 | 51 | func loadLiuData() { 52 | // 最優先用liu-uni.tab 53 | if checkFileExist(liuUniTab) { 54 | LiuUniTabConverter().convertLiuUniTab() 55 | } 56 | // 暫時優先使用json字檔,未來仍可優先使用cin字檔 57 | else if checkFileExist(liuJsonPath) { 58 | loadLiuJson() 59 | } else if checkFileExist(liuCinPath) { 60 | CinReader().readCin() 61 | } else { 62 | NotifierController.notify(message: "字檔並不存在!", stay: true) 63 | } 64 | } 65 | 66 | func loadPinyinJson() { 67 | cleanAllData("Zhuin") 68 | do { 69 | if let url = Bundle.main.url(forResource: "pinyin", withExtension: "bundle"), let bundle = Bundle(url: url), 70 | let path = bundle.path(forResource: "pinyin", ofType: "json") { 71 | let data = try Data(contentsOf: URL(fileURLWithPath: path)) 72 | if let json = try JSONSerialization.jsonObject(with: data) as? [String: [String]] { 73 | var count: Int64 = 0 74 | for (key, value) in json { 75 | for v in value { 76 | let model = NSEntityDescription.insertNewObject( 77 | forEntityName: "Zhuin", 78 | into: persistenceContainer.container.viewContext 79 | ) 80 | guard let model = model as? Zhuin else { continue } 81 | model.key = key 82 | model.value = v 83 | model.key_priority = count 84 | count += 1 85 | } 86 | } 87 | persistenceContainer.saveContext() 88 | userDefaults.set(true, forKey: "hadReadPinyinJson") 89 | NSLog("pinyin.json laoded") 90 | } 91 | } 92 | } catch { 93 | NSLog("Error: " + String(describing: error)) 94 | } 95 | } 96 | 97 | func loadLiuJson() { 98 | cleanAllData("Phrase") 99 | do { 100 | let data = try Data(contentsOf: URL(fileURLWithPath: liuJsonPath)) 101 | if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { 102 | if let chardefs = json["chardefs"] as? [String: [String]] { 103 | let res = chardefs.sorted(by: { $0.0 < $1.0 }) 104 | var count: Int64 = 0 105 | for (key, value) in res { 106 | for v in value { 107 | let model = NSEntityDescription.insertNewObject( 108 | forEntityName: "Phrase", 109 | into: persistenceContainer.container.viewContext 110 | ) 111 | guard let model = model as? Phrase else { continue } 112 | model.key_priority = count 113 | model.key = key 114 | model.value = v 115 | model.sp = false 116 | count += 1 117 | } 118 | } 119 | persistenceContainer.saveContext() 120 | userDefaults.set(true, forKey: "hadReadLiuJson") 121 | userDefaults.set(false, forKey: "isLoadByLiuUniTab") 122 | NSLog("liu.json loaded") 123 | } 124 | } 125 | } catch { 126 | AppDelegate.shared.pushInstantNotification( 127 | title: String(describing: error), 128 | subtitle: "", 129 | body: "", 130 | sound: true 131 | ) 132 | } 133 | NotifierController.notify(message: "成功匯入liu.json", stay: true) 134 | } 135 | 136 | func reloadAllData() { 137 | if checkFileExist(liuUniTab) { 138 | LiuUniTabConverter().convertLiuUniTab() 139 | } else if checkFileExist(liuJsonPath) { 140 | loadLiuJson() 141 | } else if checkFileExist(liuCinPath) { 142 | CinReader().readCin() 143 | } else { 144 | NotifierController.notify(message: "字檔並不存在!", stay: true) 145 | } 146 | loadPinyinJson() 147 | } 148 | } 149 | 150 | extension DataInitializer { 151 | func checkFileExist(_ fileName: String) -> Bool { 152 | print("[ilimi] Checking file existence: " + fileName) 153 | return FileManager.default.fileExists(atPath: fileName) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/LangModelAssembly/DataModel/CoreDataHelper.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import CoreData 6 | import Foundation 7 | 8 | class CoreDataHelper { 9 | // 取得文字的注音碼的raw value(英文碼) 10 | static func getRawZhuyinOfChar(_ text: String) -> [String] { 11 | let request = NSFetchRequest(entityName: "Zhuin") 12 | request.predicate = NSPredicate(format: "value == %@", text) 13 | do { 14 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 15 | let res = response.compactMap { $0.key } 16 | return res 17 | } catch { 18 | NSLog(error.localizedDescription) 19 | } 20 | return [] 21 | } 22 | 23 | // 查找文字的注音碼 24 | static func getZhuyinOfChar(_ char: String) -> [String] { 25 | let requestForZhuyin = NSFetchRequest(entityName: "Zhuin") 26 | requestForZhuyin.predicate = NSPredicate(format: "value == %@", char) 27 | var res: [String] = [] 28 | do { 29 | let responseForZhuyin = try PersistenceController.shared.container.viewContext.fetch(requestForZhuyin) 30 | for zhuyin in responseForZhuyin { 31 | guard let zhuyinKey = zhuyin.key else { continue } 32 | res.append(StringConverter.shared.keyToZhuyins(zhuyinKey)) 33 | } 34 | } catch { 35 | NSLog(error.localizedDescription) 36 | } 37 | return res 38 | } 39 | 40 | // 查找文字的嘸蝦米輸入碼 41 | static func getKeyOfChar(_ char: String) -> [String] { 42 | let requestForKey = NSFetchRequest(entityName: "Phrase") 43 | requestForKey.predicate = NSPredicate(format: "value == %@", char) 44 | var res: [String] = [] 45 | do { 46 | let responseForKey = try PersistenceController.shared.container.viewContext.fetch(requestForKey) 47 | for phrase in responseForKey { 48 | guard let phraseKey = phrase.key else { continue } 49 | res.append(phraseKey) 50 | } 51 | } catch { 52 | NSLog(error.localizedDescription) 53 | } 54 | return res 55 | } 56 | 57 | // 取得相同讀音的候選字 58 | static func getCharWithSamePronunciation(_ char: String) -> [String] { 59 | let zhuyins: [String] = getRawZhuyinOfChar(char) 60 | var result: [String] = [] 61 | let request = NSFetchRequest(entityName: "Zhuin") 62 | do { 63 | for zhuyin in zhuyins { 64 | request.predicate = NSPredicate(format: "key == %@", zhuyin) 65 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 66 | for item in response { 67 | if let itemValue = item.value, itemValue != char { 68 | result.append(itemValue) 69 | } 70 | } 71 | } 72 | } catch { 73 | NSLog(error.localizedDescription) 74 | } 75 | return result 76 | } 77 | 78 | // 利用注音取得文字 79 | static func getCharByZhuyin(_ text: String) -> [String] { 80 | let request = NSFetchRequest(entityName: "Zhuin") 81 | request.predicate = NSPredicate(format: "key BEGINSWITH %@", text) 82 | var result: [String] = [] 83 | request.sortDescriptors = [ 84 | NSSortDescriptor(key: "key.length", ascending: true), 85 | NSSortDescriptor(key: "key_priority", ascending: true), 86 | ] 87 | do { 88 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 89 | result = response.compactMap { $0.value } 90 | } catch { 91 | NSLog(error.localizedDescription) 92 | } 93 | return result 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/LangModelAssembly/DataModel/PersistenceController.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import CoreData 6 | import Foundation 7 | 8 | struct PersistenceController { 9 | // MARK: Lifecycle 10 | 11 | init(inMemory: Bool = false) { 12 | let modelURL = Bundle.module.url(forResource: "Model", withExtension: "momd")! 13 | let model = NSManagedObjectModel(contentsOf: modelURL)! 14 | self.container = NSPersistentContainer(name: "Model", managedObjectModel: model) 15 | if inMemory { 16 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") 17 | } 18 | container.loadPersistentStores(completionHandler: { _, error in 19 | if let error = error as NSError? { 20 | fatalError("Unresolved error \(error), \(error.userInfo)") 21 | } 22 | }) 23 | container.viewContext.automaticallyMergesChangesFromParent = true 24 | } 25 | 26 | // MARK: Internal 27 | 28 | static let shared = PersistenceController() 29 | 30 | let container: NSPersistentContainer 31 | 32 | func saveContext() { 33 | let context = container.viewContext 34 | if context.hasChanges { 35 | do { 36 | try context.save() 37 | } catch { 38 | context.rollback() 39 | let nserror = error as NSError 40 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 41 | } 42 | } 43 | } 44 | 45 | func writeData(_ key: String, _ value: String, _ priority: Int64) { 46 | let model = NSEntityDescription.insertNewObject( 47 | forEntityName: "Phrase", 48 | into: container.viewContext 49 | ) 50 | guard let model = model as? Phrase else { return } 51 | model.key_priority = priority 52 | model.key = key 53 | model.value = value 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/LangModelAssembly/WordDictReader/CinReader.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import CoreData 6 | import Foundation 7 | 8 | class CinReader { 9 | let liuCinPath = DataInitializer.appSupportDir + "/liu.cin" 10 | 11 | let persistenceContainer = PersistenceController.shared 12 | let userDefaults = UserDefaults.standard 13 | 14 | func readCin() { 15 | DataInitializer.shared.cleanAllData("Phrase") 16 | do { 17 | let contents = try String(contentsOfFile: liuCinPath) 18 | let lines = contents.split(separator: "\n") 19 | var chardefStarted = false 20 | let realLines = lines.map { 21 | sub -> String in String(sub) 22 | } 23 | var data: [[String]] = [] 24 | for line in realLines { 25 | if line == "%chardef begin" { 26 | chardefStarted = true 27 | continue 28 | } 29 | if line == "%chardef end" { 30 | chardefStarted = false 31 | break 32 | } 33 | if !chardefStarted { 34 | continue 35 | } 36 | let charDataSeq = line.split(separator: " ") 37 | let charData = charDataSeq.map { sub in 38 | String(sub) 39 | } 40 | data.append(charData) 41 | } 42 | data = data.sorted(by: { $0[0] < $1[0] }) 43 | var priority: Int64 = 0 44 | for item in data { 45 | // 如果不是「字碼+文字」就不處理 46 | if item.count == 2 { 47 | writeData(item[0], item[1], priority) 48 | priority += 1 49 | } 50 | } 51 | persistenceContainer.saveContext() 52 | // hadReadLiuJson就先不改名成hadReadLiu了... 53 | userDefaults.set(true, forKey: "hadReadLiuJson") 54 | userDefaults.set(false, forKey: "isLoadByLiuUniTab") 55 | NotifierController.notify(message: "自liu.cin讀取\(priority)個字元", stay: true) 56 | } catch { 57 | NSLog(error.localizedDescription) 58 | NotifierController.notify(message: "讀取cin字檔錯誤") 59 | } 60 | } 61 | 62 | func writeData(_ key: String, _ value: String, _ priority: Int64) { 63 | let model = NSEntityDescription.insertNewObject( 64 | forEntityName: "Phrase", 65 | into: persistenceContainer.container.viewContext 66 | ) 67 | guard let model = model as? Phrase else { return } 68 | model.key_priority = priority 69 | model.key = key 70 | model.value = value 71 | model.sp = false 72 | } 73 | 74 | func getDocumentsDirectory() -> URL { 75 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 76 | return paths[0] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/LangModelAssembly/WordDictReader/LiuUniTabConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/4/25. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | 11 | class LiuUniTabConverter { 12 | // MARK: Lifecycle 13 | 14 | init(filename: String = "liu-uni.tab") { 15 | do { 16 | let fileData = try Data(contentsOf: URL(fileURLWithPath: DataInitializer.appSupportDir + "/" + filename)) 17 | self.bytes = [UInt8](fileData) 18 | } catch { 19 | print("Failed to read file:", error) 20 | } 21 | } 22 | 23 | // MARK: Internal 24 | 25 | let persistenceContainer = PersistenceController.shared 26 | let userDefaults = UserDefaults.standard 27 | var bytes: [UInt8] = [] 28 | 29 | func getint16(addr: Int) -> Int { 30 | Int(bytes[addr]) | Int(bytes[addr + 1]) << 8 31 | } 32 | 33 | func getbits(_ start: Int, _ nbit: Int, _ i: Int) -> Int { 34 | if nbit == 1 || nbit == 2 || nbit == 4 { 35 | let byte = bytes[start + i * nbit / 8] 36 | let ovalue = Int(byte) >> (8 - nbit - i * nbit % 8) 37 | return ovalue & ((1 << nbit) - 1) 38 | } else if nbit > 0, nbit % 8 == 0 { 39 | let nbyte = nbit / 8 40 | var value = 0 41 | var a = start + i * nbyte 42 | for _ in 0 ..< nbyte { 43 | value = value << 8 | Int(bytes[a]) 44 | a += 1 45 | } 46 | return value 47 | } else { 48 | fatalError("Invalid nbit") 49 | } 50 | } 51 | 52 | func utf8_chr(ord: Int) -> String { 53 | String(UnicodeScalar(ord)!) 54 | } 55 | 56 | func mb_str_split(_ str: String) -> [String] { 57 | str.map { String($0) } 58 | } 59 | 60 | func convertLiuUniTab() { 61 | DataInitializer.shared.cleanAllData("Phrase") 62 | let i1 = getint16(addr: 0) 63 | _ = getint16(addr: 4) 64 | let i2 = i1 + getint16(addr: 2) // or + (words*2+7)/8 65 | let i3 = i2 + getint16(addr: 6) // or + (words*1+7)/8 66 | let i4 = i3 + getint16(addr: 6) // or + (words*1+7)/8 67 | 68 | let rootkey: [Character] = Array(" abcdefghijklmnopqrstuvwxyz,.'[]") 69 | 70 | var count = 0 71 | for i in 0 ..< 1024 { 72 | var key = [Character](repeating: Character(" "), count: 4) 73 | 74 | key[0] = rootkey[i / 32] 75 | key[1] = rootkey[i % 32] 76 | 77 | if key[0] == " " { continue } 78 | 79 | for ci in getint16(addr: i * 2) ..< getint16(addr: i * 2 + 2) { 80 | let bit24 = getbits(i4, 24, ci) 81 | let hi = getbits(i1, 2, ci) 82 | let lo = bit24 & 0x3fff 83 | key[2] = rootkey[bit24 >> 19] 84 | key[3] = rootkey[bit24 >> 14 & 0x1f] 85 | 86 | let keyString = String(key).trimmingCharacters(in: .whitespacesAndNewlines) 87 | let chr = utf8_chr(ord: hi << 14 | lo) 88 | 89 | let flag_sp = getbits(i3, 1, ci) 90 | writeData(keyString, chr, Int64(count), flag_sp == 0 ? true : false) 91 | // let flag_unknown = getbits(start: Int(i2), nbit: 1, i: Int(ci)) 92 | count += 1 93 | } 94 | } 95 | persistenceContainer.saveContext() 96 | userDefaults.set(true, forKey: "hadReadLiuJson") 97 | userDefaults.set(true, forKey: "isLoadByLiuUniTab") 98 | NotifierController.notify(message: "自liu-uni.tab讀取\(count)個字元") 99 | } 100 | 101 | func writeData(_ key: String, _ value: String, _ priority: Int64, _ sp: Bool) { 102 | let model = NSEntityDescription.insertNewObject( 103 | forEntityName: "Phrase", 104 | into: persistenceContainer.container.viewContext 105 | ) 106 | guard let model = model as? Phrase else { return } 107 | model.key_priority = priority 108 | model.key = key 109 | model.value = value 110 | model.sp = sp 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/AutoCheckUpdate/Model/CustomError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/5/7. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - CustomError 11 | 12 | public enum CustomError { 13 | case noData, noConnection 14 | } 15 | 16 | // MARK: LocalizedError 17 | 18 | extension CustomError: LocalizedError { 19 | public var errorDescription: String? { 20 | switch self { 21 | case .noData: 22 | return "no data" 23 | case .noConnection: 24 | return "no connection" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/AutoCheckUpdate/Model/GithubRelease.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/5/8. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GitHubRelease: Codable { 11 | let tagName: String 12 | let htmlUrl: String 13 | let name: String 14 | let id: Int 15 | } 16 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/AutoCheckUpdate/UpdateManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/5/7. 6 | // 7 | 8 | import AppKit 9 | import Foundation 10 | 11 | // MARK: - UpdateManager 12 | 13 | class UpdateManager { 14 | // MARK: Internal 15 | 16 | static let url = URL(string: "https://api.github.com/repos/y1lichen/ilimi-inputmethod/releases/latest") 17 | 18 | static func getURLData(completion: @escaping (Result) -> Void) { 19 | let session = URLSession.shared 20 | 21 | // 創建 URLSessionDataTask 22 | let task = session.dataTask(with: url!) { data, response, error in 23 | // 檢查是否有錯誤 24 | if let error = error { 25 | print("發生錯誤:", error) 26 | completion(.failure(error)) 27 | return 28 | } 29 | 30 | // 檢查是否有回應 31 | guard let httpResponse = response as? HTTPURLResponse else { 32 | print("無效的回應") 33 | return 34 | } 35 | 36 | // 檢查狀態碼 37 | guard httpResponse.statusCode == 200 else { 38 | print("無效的狀態碼:", httpResponse.statusCode) 39 | return 40 | } 41 | 42 | // 檢查是否有資料 43 | guard let data = data else { 44 | print("未收到資料") 45 | completion(.failure(CustomError.noData)) 46 | return 47 | } 48 | 49 | // 解析 JSON 回應 50 | do { 51 | let decoder = JSONDecoder() 52 | decoder.keyDecodingStrategy = .convertFromSnakeCase 53 | let release = try decoder.decode(GitHubRelease.self, from: data) 54 | 55 | completion(.success(release)) 56 | // for asset in release.assets { 57 | // print("- 名稱:", asset.name) 58 | // print(" 下載連結:", asset.downloadUrl) 59 | // } 60 | } catch { 61 | completion(.failure(error)) 62 | } 63 | } 64 | 65 | // 開始任務 66 | task.resume() 67 | } 68 | 69 | // 自動檢查更新 70 | static func autoCheckUpdate() { 71 | let autoCheckUpdate: Bool = UserDefaults.standard.bool(forKey: "autoCheckUpdate") 72 | if !autoCheckUpdate { 73 | return 74 | } 75 | let lastCheckUpdate = UserDefaults.standard.object(forKey: "lastCheckUpdate") as? Date 76 | let now = Date() 77 | if lastCheckUpdate != nil { 78 | let interval = now.timeIntervalSince(lastCheckUpdate!) 79 | let hour = interval / 3600 80 | // 12小時檢查一次是否有新版 81 | if hour >= 12 { 82 | checkUpdate(isManual: false) 83 | } 84 | } 85 | UserDefaults.standard.setValue(now, forKey: "lastCheckUpdate") 86 | } 87 | 88 | static func checkUpdate(isManual: Bool) { 89 | var appVer = "" 90 | if let infoDict = Bundle.main.infoDictionary { 91 | if !infoDict.isEmpty { 92 | appVer = infoDict["CFBundleShortVersionString"] as! String? ?? "unkown" 93 | } 94 | } 95 | getURLData { result in 96 | switch result { 97 | case let .success(data): 98 | DispatchQueue.main.async { 99 | showPopUp(appVer, data.tagName, isManual, data.htmlUrl) 100 | } 101 | 102 | case let .failure(error): 103 | DispatchQueue.main.async { 104 | NotifierController.notify(message: error.localizedDescription) 105 | } 106 | } 107 | } 108 | } 109 | 110 | // MARK: Private 111 | 112 | // https://leetcode.com/problems/compare-version-numbers/description/ 113 | // 參考leetcode吧 114 | private static func compareVersion(_ version1: String, _ version2: String) -> Int { 115 | let v1Components = version1.components(separatedBy: ".") 116 | let v2Components = version2.components(separatedBy: ".") 117 | 118 | var i = 0 119 | 120 | while i < max(v1Components.count, v2Components.count) { 121 | let v1 = i < v1Components.count ? Int(v1Components[i]) ?? 0 : 0 122 | let v2 = i < v2Components.count ? Int(v2Components[i]) ?? 0 : 0 123 | 124 | if v1 < v2 { 125 | return -1 126 | } else if v1 > v2 { 127 | return 1 128 | } 129 | i += 1 130 | } 131 | return 0 132 | } 133 | 134 | private static func showPopUp(_ appVer: String, _ remoteVer: String, _ isManual: Bool, _ url: String) { 135 | let res = compareVersion(appVer, remoteVer) 136 | if !isManual, res == 0 { 137 | return 138 | } 139 | var message = "你己經擁有最新版本\(appVer)" 140 | if res == 1 { 141 | message = "你擁有的是測試版本\(appVer)。目前發佈版本為\(remoteVer)。" 142 | } else if res == -1 { 143 | message = "最新版本為\(remoteVer),你擁有的是版本\(appVer)。前往更新吧!" 144 | } 145 | // 只有手動更新時才要通知使用的是測試版 146 | if !isManual && res == 1 { 147 | return 148 | } 149 | let alert = NSAlert() 150 | alert.messageText = "最新版本為\(remoteVer)" 151 | alert.informativeText = message 152 | if res == 1 || res == -1 { 153 | alert.addButton(withTitle: "前往下載") 154 | alert.addButton(withTitle: "略過") 155 | } 156 | NSApp.activate(ignoringOtherApps: true) 157 | let modalResult = alert.runModal() 158 | if modalResult == .alertFirstButtonReturn { 159 | if let url = URL(string: url) { 160 | NSWorkspace.shared.open(url) 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/CustomPhrase/CustomPhraseManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/4/12. 6 | // 7 | 8 | import CoreData 9 | 10 | public class CustomPhraseManager { 11 | // MARK: Lifecycle 12 | 13 | // MARK: - Initializer 14 | 15 | private init() {} 16 | 17 | // MARK: Public 18 | 19 | public static var context: NSManagedObjectContext { 20 | persistenceController.container.viewContext 21 | } 22 | 23 | // MARK: - Core Data stack 24 | 25 | // MARK: - Core Data Saving support 26 | 27 | // MARK: Internal 28 | 29 | // MARK: - Define Constants / Variables 30 | 31 | static var persistenceController = PersistenceController.shared 32 | static let defaultPhraseDict = ["oaooo": "哈哈哈", "ilimi": "一粒米輸入法"] 33 | 34 | static func getAllCustomPhrase() -> [CustomPhrase] { 35 | let request = NSFetchRequest(entityName: "CustomPhrase") 36 | request.sortDescriptors = [NSSortDescriptor(keyPath: \CustomPhrase.timestp, ascending: true)] 37 | do { 38 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 39 | return response 40 | } catch { 41 | NSLog(error.localizedDescription) 42 | } 43 | return [] 44 | } 45 | 46 | static func getCustomPhraseByKey(_ key: String) -> [CustomPhrase] { 47 | let request = NSFetchRequest(entityName: "CustomPhrase") 48 | request.sortDescriptors = [NSSortDescriptor(keyPath: \CustomPhrase.timestp, ascending: true)] 49 | request.predicate = SettingViewModel.shared 50 | .showOnlyExactlyMatch ? NSPredicate(format: "key == %@", key) : 51 | NSPredicate(format: "key BEGINSWITH %@", key) 52 | do { 53 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 54 | return response 55 | } catch { 56 | NSLog(error.localizedDescription) 57 | } 58 | return [] 59 | } 60 | 61 | static func setDefaultCustomPhrase() { 62 | cleanAllData() 63 | for (key, value) in defaultPhraseDict { 64 | let model = NSEntityDescription.insertNewObject( 65 | forEntityName: "CustomPhrase", 66 | into: context 67 | ) 68 | guard let model = model as? CustomPhrase else { return } 69 | model.key = key 70 | model.value = value 71 | model.timestp = Date.now 72 | } 73 | persistenceController.saveContext() 74 | } 75 | 76 | static func addCustomPhrase(key: String, value: String) { 77 | let model = CustomPhrase(context: context) 78 | model.key = key 79 | model.value = value 80 | model.timestp = Date.now 81 | persistenceController.saveContext() 82 | } 83 | 84 | static func deleteCustomPhrase(_ phrase: CustomPhrase) { 85 | context.delete(phrase) 86 | persistenceController.saveContext() 87 | } 88 | 89 | static func editCustomPhrase(_ phrase: CustomPhrase, key: String, value: String) { 90 | phrase.key = key 91 | phrase.value = value 92 | persistenceController.saveContext() 93 | } 94 | 95 | static func cleanAllData() { 96 | let fetchRequest = NSFetchRequest(entityName: "CustomPhrase") 97 | let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 98 | do { 99 | try context.execute(batchDeleteRequest) 100 | } catch { 101 | NSLog(String(describing: error)) 102 | } 103 | NSLog("Core Data cleaned") 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/FullWidthMode/FullWidthMode.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import InputMethodKit 6 | 7 | extension IlimiInputController { 8 | func handleFullWidthMode(event: NSEvent, client sender: Any!) -> Bool { 9 | if !isFullWidthMode { 10 | return false 11 | } 12 | if let inputChar = event.characters?.first { 13 | commitText(client: sender, text: String(inputChar).fullWidth) 14 | } 15 | return true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/InputEngine/InputContext.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Foundation 6 | 7 | class InputContext { 8 | // MARK: Internal 9 | 10 | static let shared = InputContext() 11 | 12 | let closureDict: [String: String] = [ 13 | "「": "」", 14 | "(": ")", 15 | "﹁": "﹂", 16 | "﹃": "﹄", 17 | "【": "】", 18 | "︻": "︼", 19 | "︵": "︶", 20 | "〈": "〉", 21 | "『": "』", 22 | "《": "》", 23 | "︽": "︾", 24 | "«": "»", 25 | ] 26 | var currentIndex: Int = 0 27 | var candidatesCount = 0 28 | 29 | var preInputPrefixSet: Set = [] 30 | // 當前候選字頁碼 31 | var candidatesPageId = 0 32 | var candidatesPagesCount = 0 33 | var closureStack: [String] = [] 34 | 35 | var isTradToSim = false { 36 | didSet { 37 | NotifierController.notify(message: isTradToSim ? "開啟打繁出簡" : "關閉打繁出簡") 38 | } 39 | } 40 | 41 | var isSpMode = false { 42 | didSet { 43 | NotifierController.notify(message: isSpMode ? "開啟快打模式" : "關閉快打模式") 44 | } 45 | } 46 | 47 | var candidates: [String] { 48 | get { _candidates } 49 | set { 50 | _candidates = newValue 51 | candidatesCount = _candidates.count 52 | IlimiInputController.prefixHasCandidates = (candidatesCount > 0) ? true : false 53 | _numberedCandidates = [] 54 | for i in 0 ..< _candidates.count { 55 | _numberedCandidates.append("\(i + 1) \(_candidates[i])") 56 | } 57 | candidatesPagesCount = candidatesCount % 9 > 0 ? (candidatesCount / 9) + 1 : candidatesCount / 9 58 | } 59 | } 60 | 61 | var numberedCandidates: [String] { 62 | _numberedCandidates 63 | } 64 | 65 | var currentNumberedCandidate: String { 66 | if currentIndex >= 0, currentIndex < _numberedCandidates.count { 67 | return _numberedCandidates[currentIndex] 68 | } 69 | return "" 70 | } 71 | 72 | func getClosingClosure() -> String? { 73 | if currentInput.isEmpty, !closureStack.isEmpty { 74 | let closure = closureStack.removeLast() 75 | if let closingClosure = closureDict[closure] { 76 | return closingClosure 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | func cleanUp() { 83 | currentIndex = 0 84 | currentInput = "" 85 | preInputPrefixSet = [] 86 | candidates = [] 87 | candidatesPageId = 0 88 | IlimiInputController.prefixHasCandidates = true 89 | } 90 | 91 | func isClosure(input: String) -> Bool { 92 | closureDict[input] != nil 93 | } 94 | 95 | func appendCurrentInput(_ inputStr: String) { 96 | currentInput.append(inputStr) 97 | } 98 | 99 | func getCurrentInput() -> String { 100 | currentInput 101 | } 102 | 103 | func deleteLastOfCurrentInput() { 104 | currentInput.removeLast() 105 | } 106 | 107 | func moveNext() { 108 | if currentIndex < candidatesCount - 1 { 109 | currentIndex += 1 110 | } 111 | } 112 | 113 | func movePrev() { 114 | if currentIndex > 0 { 115 | currentIndex -= 1 116 | } 117 | } 118 | 119 | func movePrevPage() { 120 | if candidatesPageId > 0 { 121 | candidatesPageId -= 1 122 | currentIndex -= 9 123 | } 124 | } 125 | 126 | func moveNextPage() { 127 | if candidatesPageId < candidatesPagesCount - 1 { 128 | candidatesPageId += 1 129 | } 130 | if 9 * candidatesPageId + currentIndex >= candidatesCount { 131 | currentIndex = candidatesCount - 1 132 | } else { 133 | currentIndex = 9 * candidatesPageId + currentIndex 134 | } 135 | } 136 | 137 | // 直式選字窗比較複雜,不處理 138 | func handleRight() { 139 | if SettingViewModel.shared.isHorizontalCandidatesPanel { 140 | moveNext() 141 | } 142 | } 143 | 144 | func handleLeft() { 145 | if SettingViewModel.shared.isHorizontalCandidatesPanel { 146 | movePrev() 147 | } 148 | } 149 | 150 | func handleUp() { 151 | if SettingViewModel.shared.isHorizontalCandidatesPanel { 152 | movePrevPage() 153 | } 154 | } 155 | 156 | func handleDown() { 157 | if SettingViewModel.shared.isHorizontalCandidatesPanel { 158 | moveNextPage() 159 | } 160 | } 161 | 162 | // MARK: Private 163 | 164 | private var currentInput: String = "" 165 | private var _candidates: [String] = [] 166 | private var _numberedCandidates: [String] = [] 167 | } 168 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/InputEngine/InputEngine.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | import CoreData 7 | import Foundation 8 | import SwiftUI 9 | 10 | struct InputEngine { 11 | static let shared = InputEngine() 12 | 13 | // 取得以注音輸入的候選字 14 | func getCadidatesByZhuyin(_ text: String) { 15 | InputContext.shared.candidates = CoreDataHelper.getCharByZhuyin(text) 16 | } 17 | 18 | func setCandidates(_ phrases: [Phrase], _ text: String) { 19 | var candidates: [String] = [] 20 | var candidatesSet: Set = [] 21 | var inputStrSet: Set = [] 22 | 23 | for r in phrases { 24 | let value: String = r.value(forKey: "value") as! String 25 | if let rKey = r.key, rKey.count > text.count { 26 | inputStrSet.insert(String(rKey.prefix(text.count + 1))) 27 | } 28 | if candidatesSet.contains(value) { 29 | continue 30 | } 31 | candidatesSet.insert(value) 32 | candidates.append(value) 33 | } 34 | // 自定義的字詞 35 | let customPhrases = CustomPhraseManager.getCustomPhraseByKey(text) 36 | for c in customPhrases { 37 | guard let phraseValue = c.value else { return } 38 | candidates.append(phraseValue) 39 | candidatesSet.insert(phraseValue) 40 | inputStrSet.insert(String(phraseValue.prefix(text.count + 1))) 41 | } 42 | InputContext.shared.preInputPrefixSet = inputStrSet 43 | InputContext.shared.candidates = candidates 44 | } 45 | 46 | // 取得以嘸蝦米輸入的候選字 47 | func getCandidates(_ text: String) { 48 | // 輸入碼太長的話就不用查詢,節省資源 49 | if text.count >= 6 { 50 | InputContext.shared.preInputPrefixSet = [] 51 | InputContext.shared.candidates = [] 52 | } 53 | 54 | var candidates: [String] = [] 55 | var candidatesSet: Set = [] 56 | var inputStrSet: Set = [] 57 | 58 | let response: [Phrase] = LiuManager.shared.getNormalModePhrase(text) 59 | for r in response { 60 | let value: String = r.value(forKey: "value") as! String 61 | if let rKey = r.key, rKey.count > text.count { 62 | inputStrSet.insert(String(rKey.prefix(text.count + 1))) 63 | } 64 | if candidatesSet.contains(value) { 65 | continue 66 | } 67 | candidatesSet.insert(value) 68 | candidates.append(value) 69 | } 70 | // 自定義的字詞 71 | let customPhrases = CustomPhraseManager.getCustomPhraseByKey(text) 72 | for c in customPhrases { 73 | guard let phraseValue = c.value else { return } 74 | candidates.append(phraseValue) 75 | candidatesSet.insert(phraseValue) 76 | inputStrSet.insert(String(phraseValue.prefix(text.count + 1))) 77 | } 78 | InputContext.shared.preInputPrefixSet = inputStrSet 79 | InputContext.shared.candidates = candidates 80 | } 81 | 82 | // 取得相同讀音的候選字 83 | func getCandidatesByPronunciation(_ text: String) { 84 | InputContext.shared.candidates = CoreDataHelper.getCharWithSamePronunciation(text) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/KeyEvent/CapsLockToggler.swift: -------------------------------------------------------------------------------- 1 | // Ref: https://stackoverflow.com/a/75870807/4162914 2 | 3 | // #import 4 | // #import 5 | 6 | import IOKitCHeaders 7 | 8 | // MARK: - CapsLockToggler 9 | 10 | public enum CapsLockToggler { 11 | public static var isOn: Bool { 12 | var state = false 13 | try? IOKit.handleHIDSystemService { ioConnect in 14 | IOHIDGetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), &state) 15 | } 16 | return state 17 | } 18 | 19 | public static func toggle() { 20 | try? IOKit.handleHIDSystemService { ioConnect in 21 | var state = false 22 | IOHIDGetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), &state) 23 | state.toggle() 24 | IOHIDSetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), state) 25 | } 26 | } 27 | 28 | public static func turnOff() { 29 | try? IOKit.handleHIDSystemService { ioConnect in 30 | IOHIDSetModifierLockState(ioConnect, Int32(kIOHIDCapsLockState), false) 31 | } 32 | } 33 | } 34 | 35 | // MARK: - IOKit 36 | 37 | // Refactored by Shiki Suen (MIT License) 38 | public enum IOKit { 39 | public static func handleHIDSystemService(_ taskHandler: @escaping (io_connect_t) -> Void) throws { 40 | let ioService: io_service_t = IOServiceGetMatchingService(0, IOServiceMatching(kIOHIDSystemClass)) 41 | var connect: io_connect_t = 0 42 | let x = IOServiceOpen(ioService, mach_task_self_, UInt32(kIOHIDParamConnectType), &connect) 43 | if let errorOne = Mach.KernReturn(rawValue: x), errorOne != .success { 44 | throw errorOne 45 | } 46 | taskHandler(connect) 47 | let y = IOServiceClose(connect) 48 | if let errorTwo = Mach.KernReturn(rawValue: y), errorTwo != .success { 49 | throw errorTwo 50 | } 51 | } 52 | } 53 | 54 | // MARK: - Mach 55 | 56 | // Refactored by Shiki Suen (MIT License) 57 | public enum Mach { 58 | public enum KernReturn: Int32, Error { 59 | case success = 0 60 | case invalidAddress = 1 61 | case protectionFailure = 2 62 | case noSpace = 3 63 | case invalidArgument = 4 64 | case failure = 5 65 | case resourceShortage = 6 66 | case notReceiver = 7 67 | case noAccess = 8 68 | case memoryFailure = 9 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/KeyEvent/NSEventImpl.swift: -------------------------------------------------------------------------------- 1 | // (c) 2021 and onwards The vChewing Project (MIT-NTL License). 2 | // ==================== 3 | // This code is released under the MIT license (SPDX-License-Identifier: MIT) 4 | // ... with NTL restriction stating that: 5 | // No trademark license is granted to use the trade names, trademarks, service 6 | // marks, or product names of Contributor, except as required to fulfill notice 7 | // requirements defined in MIT License. 8 | 9 | import AppKit 10 | 11 | // MARK: - NSEvent Extension - Modified Flags 12 | 13 | extension NSEvent { 14 | public var keyModifierFlags: ModifierFlags { 15 | modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock) 16 | } 17 | 18 | public var commonKeyModifierFlags: ModifierFlags { 19 | keyModifierFlags.subtracting([.function, .numericPad, .help]) 20 | } 21 | } 22 | 23 | // MARK: - NSEvent Extension - Reconstructors 24 | 25 | extension NSEvent { 26 | public func reinitiate( 27 | with type: NSEvent.EventType? = nil, 28 | location: NSPoint? = nil, 29 | modifierFlags: NSEvent.ModifierFlags? = nil, 30 | timestamp: TimeInterval? = nil, 31 | windowNumber: Int? = nil, 32 | characters: String? = nil, 33 | charactersIgnoringModifiers: String? = nil, 34 | isARepeat: Bool? = nil, 35 | keyCode: UInt16? = nil 36 | ) 37 | -> NSEvent? { 38 | let oldChars: String = type == .flagsChanged ? "" : characters ?? "" 39 | var characters = characters 40 | checkSpecialKey: if let matchedKey = KeyCode(rawValue: keyCode ?? self.keyCode) { 41 | let scalar = matchedKey.correspondedSpecialKeyScalar(flags: modifierFlags ?? self.modifierFlags) 42 | guard let scalar = scalar else { break checkSpecialKey } 43 | characters = .init(scalar) 44 | } 45 | 46 | return NSEvent.keyEvent( 47 | with: type ?? self.type, 48 | location: location ?? locationInWindow, 49 | modifierFlags: modifierFlags ?? self.modifierFlags, 50 | timestamp: timestamp ?? self.timestamp, 51 | windowNumber: windowNumber ?? self.windowNumber, 52 | context: nil, 53 | characters: characters ?? oldChars, 54 | charactersIgnoringModifiers: charactersIgnoringModifiers ?? characters ?? oldChars, 55 | isARepeat: isARepeat ?? self.isARepeat, 56 | keyCode: keyCode ?? self.keyCode 57 | ) 58 | } 59 | } 60 | 61 | // MARK: - KeyCode 62 | 63 | // Use KeyCodes as much as possible since its recognition won't be affected by macOS Base Keyboard Layouts. 64 | // KeyCodes: https://eastmanreference.com/complete-list-of-applescript-key-codes 65 | // Also: HIToolbox.framework/Versions/A/Headers/Events.h 66 | public enum KeyCode: UInt16 { 67 | case kNone = 0 68 | case kCarriageReturn = 36 // Renamed from "kReturn" to avoid nomenclatural confusions. 69 | case kTab = 48 70 | case kSpace = 49 71 | case kSymbolMenuPhysicalKeyIntl = 50 // vChewing Specific (Non-JIS) 72 | case kBackSpace = 51 // Renamed from "kDelete" to avoid nomenclatural confusions. 73 | case kEscape = 53 74 | case kCommand = 55 75 | case kShift = 56 76 | case kCapsLock = 57 77 | case kOption = 58 78 | case kControl = 59 79 | case kRightShift = 60 80 | case kRightOption = 61 81 | case kRightControl = 62 82 | case kFunction = 63 83 | case kF17 = 64 84 | case kVolumeUp = 72 85 | case kVolumeDown = 73 86 | case kMute = 74 87 | case kLineFeed = 76 // Another keyCode to identify the Enter Key, typable by Fn+Enter. 88 | case kF18 = 79 89 | case kF19 = 80 90 | case kF20 = 90 91 | case kYen = 93 92 | case kSymbolMenuPhysicalKeyJIS = 94 // vChewing Specific (JIS) 93 | case kJISNumPadComma = 95 94 | case kF5 = 96 95 | case kF6 = 97 96 | case kF7 = 98 97 | case kF3 = 99 98 | case kF8 = 100 99 | case kF9 = 101 100 | case kJISAlphanumericalKey = 102 101 | case kF11 = 103 102 | case kJISKanaSwappingKey = 104 103 | case kF13 = 105 // PrtSc 104 | case kF16 = 106 105 | case kF14 = 107 106 | case kF10 = 109 107 | case kContextMenu = 110 108 | case kF12 = 111 109 | case kF15 = 113 110 | case kHelp = 114 // Insert 111 | case kHome = 115 112 | case kPageUp = 116 113 | case kWindowsDelete = 117 // Renamed from "kForwardDelete" to avoid nomenclatural confusions. 114 | case kF4 = 118 115 | case kEnd = 119 116 | case kF2 = 120 117 | case kPageDown = 121 118 | case kF1 = 122 119 | case kLeftArrow = 123 120 | case kRightArrow = 124 121 | case kDownArrow = 125 122 | case kUpArrow = 126 123 | 124 | // MARK: Public 125 | 126 | public func correspondedSpecialKeyScalar(flags: NSEvent.ModifierFlags) -> Unicode.Scalar? { 127 | var rawData: NSEvent.SpecialKey? { 128 | switch self { 129 | case .kNone: return nil 130 | case .kCarriageReturn: return .carriageReturn 131 | case .kTab: return flags.contains(.shift) ? .backTab : .tab 132 | case .kSpace: return nil 133 | case .kSymbolMenuPhysicalKeyIntl: return nil 134 | case .kBackSpace: return .backspace 135 | case .kEscape: return nil 136 | case .kCommand: return nil 137 | case .kShift: return nil 138 | case .kCapsLock: return nil 139 | case .kOption: return nil 140 | case .kControl: return nil 141 | case .kRightShift: return nil 142 | case .kRightOption: return nil 143 | case .kRightControl: return nil 144 | case .kFunction: return nil 145 | case .kF17: return .f17 146 | case .kVolumeUp: return nil 147 | case .kVolumeDown: return nil 148 | case .kMute: return nil 149 | case .kLineFeed: return nil // TODO: return 待釐清 150 | case .kF18: return .f18 151 | case .kF19: return .f19 152 | case .kF20: return .f20 153 | case .kYen: return nil 154 | case .kSymbolMenuPhysicalKeyJIS: return nil 155 | case .kJISNumPadComma: return nil 156 | case .kF5: return .f5 157 | case .kF6: return .f6 158 | case .kF7: return .f7 159 | case .kF3: return .f7 160 | case .kF8: return .f8 161 | case .kF9: return .f9 162 | case .kJISAlphanumericalKey: return nil 163 | case .kF11: return .f11 164 | case .kJISKanaSwappingKey: return nil 165 | case .kF13: return .f13 166 | case .kF16: return .f16 167 | case .kF14: return .f14 168 | case .kF10: return .f10 169 | case .kContextMenu: return .menu 170 | case .kF12: return .f12 171 | case .kF15: return .f15 172 | case .kHelp: return .help 173 | case .kHome: return .home 174 | case .kPageUp: return .pageUp 175 | case .kWindowsDelete: return .deleteForward 176 | case .kF4: return .f4 177 | case .kEnd: return .end 178 | case .kF2: return .f2 179 | case .kPageDown: return .pageDown 180 | case .kF1: return .f1 181 | case .kLeftArrow: return .leftArrow 182 | case .kRightArrow: return .rightArrow 183 | case .kDownArrow: return .downArrow 184 | case .kUpArrow: return .upArrow 185 | } 186 | } 187 | return rawData?.unicodeScalar 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/NormalMode/LiuManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/6/9. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct LiuManager { 13 | static let shared = LiuManager() 14 | 15 | @StateObject var settingsModel = SettingViewModel.shared 16 | 17 | func getPhraseExactly(_ text: String) -> [Phrase] { 18 | let request = NSFetchRequest(entityName: "Phrase") 19 | request.predicate = NSPredicate(format: "key == %@", text) 20 | request.sortDescriptors = [NSSortDescriptor(key: "key_priority", ascending: true)] 21 | do { 22 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 23 | return response 24 | } catch { 25 | NSLog(error.localizedDescription) 26 | } 27 | return [] 28 | } 29 | 30 | func getNormalModePhrase(_ text: String) -> [Phrase] { 31 | let request = NSFetchRequest(entityName: "Phrase") 32 | request.predicate = settingsModel 33 | .showOnlyExactlyMatch ? NSPredicate(format: "key == %@", text) : 34 | NSPredicate(format: "key BEGINSWITH %@", text) 35 | request.sortDescriptors = [NSSortDescriptor(key: "key_priority", ascending: true)] 36 | do { 37 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 38 | return response 39 | } catch { 40 | NSLog(error.localizedDescription) 41 | } 42 | return [] 43 | } 44 | 45 | func getKeysOfChar(_ chr: String) -> [Phrase] { 46 | let request = NSFetchRequest(entityName: "Phrase") 47 | request.predicate = NSPredicate(format: "value == %@", chr) 48 | do { 49 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 50 | return response 51 | } catch { 52 | NSLog(error.localizedDescription) 53 | } 54 | return [] 55 | } 56 | 57 | func checkIsFirstCandidates(_ input: String, _ chr: String) -> Bool { 58 | getPhraseExactly(input).first?.value == chr 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/Notification/Beep.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | 7 | extension IlimiInputController { 8 | func beep() { 9 | let isSilentMode = SettingViewModel.shared.silentMode 10 | if isSilentMode { 11 | return 12 | } 13 | NSSound.beep() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/Notification/Notification.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Foundation 6 | import UserNotifications 7 | 8 | extension AppDelegate: UNUserNotificationCenterDelegate { 9 | // 推送系統通知 10 | func pushInstantNotification(title: String, subtitle: String, body: String, sound: Bool) { 11 | userNotificationCenter.getNotificationSettings { settings in 12 | guard settings.authorizationStatus == .authorized else { 13 | return 14 | } 15 | let content = UNMutableNotificationContent() 16 | content.title = title 17 | content.subtitle = subtitle 18 | content.body = body 19 | if sound { 20 | content.sound = UNNotificationSound.default 21 | } 22 | // 使用uuid做為identifier 23 | let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) 24 | self.userNotificationCenter.add(request) { _ in } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/SamePronunciationMode/SamePronunciationMode.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Foundation 6 | 7 | extension IlimiInputController { 8 | // 輸入\進入同音輸入模式 9 | func checkIsInputByPronunciationMode(_ input: String) -> Bool { 10 | isTypeByPronunciationMode = (input == "\\") 11 | if isTypeByPronunciationMode { 12 | InputContext.shared.cleanUp() 13 | client().setMarkedText("音", selectionRange: notFoundRange, replacementRange: notFoundRange) 14 | return true 15 | } 16 | return false 17 | } 18 | 19 | // 取得同音輸入模式的同音候選字 20 | func getNewCandidatesOfSamePronunciation(text: String, client sender: Any!) { 21 | InputEngine.shared.getCandidatesByPronunciation(text) 22 | if InputContext.shared.candidatesCount > 0 { 23 | isSecondCommitOfTypeByPronunciationMode = true 24 | candidates.update() 25 | candidates.show() 26 | ensureWindowLevel(client: sender) 27 | } else { 28 | // 沒有同音字時直接輸入該文字 29 | client().insertText(text, replacementRange: NSRange(location: 0, length: 2)) 30 | turnOffIsInputByPronunciationMode() 31 | // 提示使用者 32 | NotifierController.notify(message: "沒有其他同音字") 33 | InputContext.shared.cleanUp() 34 | candidates.hide() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/SpMode/SpModeManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/5/2. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | 11 | class SpModeManager { 12 | // MARK: Internal 13 | 14 | static func getSpKeyOfChar(_ chr: String) -> [String] { 15 | let isLoadByLiu = UserDefaults.standard.bool(forKey: "isLoadByLiuUniTab") 16 | if isLoadByLiu { 17 | return getSpOfCharWithLiuTab(chr) 18 | } 19 | return getSpOfCharWithoutLiuTab(chr) 20 | } 21 | 22 | static func checkInputIsSp(_ text: String, _ assistChar: String) -> Bool { 23 | // 在快打模式下標點符號不一定要是最短碼 24 | if InputContext.shared.getCurrentInput().first == "," { 25 | return true 26 | } 27 | let isLoadByLiu = UserDefaults.standard.bool(forKey: "isLoadByLiuUniTab") 28 | var input = InputContext.shared.getCurrentInput() 29 | if !assistChar.isEmpty { 30 | input.removeLast() 31 | } 32 | if isLoadByLiu, !getSpOfCharWithLiuTab(text).contains(input) { 33 | return false 34 | } 35 | if !isLoadByLiu, !getSpOfCharWithoutLiuTab(text).contains(input) { 36 | // if handleMultipleSp(text, isLoadByLiu) { 37 | // return true 38 | // } 39 | return false 40 | } 41 | return true 42 | } 43 | 44 | static func getIndexOfChr(_ input: String, _ chr: String) -> Int { 45 | let request = NSFetchRequest(entityName: "Phrase") 46 | request.predicate = NSPredicate(format: "key BEGINSWITH %@", input) 47 | request.sortDescriptors = [NSSortDescriptor(key: "key_priority", ascending: true)] 48 | do { 49 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 50 | for i in 0 ... response.count { 51 | if response[i].value == chr { 52 | return i 53 | } 54 | } 55 | } catch { 56 | NSLog(error.localizedDescription) 57 | } 58 | return -1 59 | } 60 | 61 | static func getSpOfCharWithoutLiuTab(_ chr: String) -> [String] { 62 | let request = NSFetchRequest(entityName: "Phrase") 63 | request.predicate = NSPredicate(format: "value == %@", chr) 64 | request.sortDescriptors = [NSSortDescriptor(key: "key_priority", ascending: true)] 65 | do { 66 | var shortestKeySet = Set() 67 | var response = try PersistenceController.shared.container.viewContext.fetch(request) 68 | response = response.sorted { phr1, phr2 in 69 | phr1.key?.count ?? 0 < phr2.key?.count ?? 0 70 | } 71 | let shortestLen: Int = response.first?.key?.count ?? -1 72 | for item in response { 73 | if item.key?.count == shortestLen { 74 | shortestKeySet.insert(item.key ?? "") 75 | } else { 76 | break 77 | } 78 | } 79 | 80 | // 如果最短字根只有一個且是第一位就直接回傳 81 | if shortestKeySet.count == 1 { 82 | var res = [shortestKeySet.first ?? ""] 83 | // 快打模式下簡碼加選字碼可能和非簡碼有相同碼數 84 | if !LiuManager.shared.checkIsFirstCandidates(shortestKeySet.first ?? "", chr) { 85 | let keys = LiuManager.shared.getKeysOfChar(chr) 86 | for item in keys { 87 | let key = item.key ?? "" 88 | if key.count == shortestLen + 1, LiuManager.shared.checkIsFirstCandidates(key, chr) { 89 | res.append(item.key ?? "") 90 | } 91 | } 92 | } 93 | return res 94 | } 95 | // 如果最短字根不只一個就去比較這個字元在哪個字根priority比較早 96 | var curPriority = 100 97 | var res: [String] = [] 98 | for keyItem in shortestKeySet { 99 | let idx = getIndexOfChr(keyItem, chr) 100 | if idx < curPriority { 101 | curPriority = idx 102 | res = [keyItem] 103 | } else if idx == curPriority { 104 | res.append(keyItem) 105 | } 106 | } 107 | return res 108 | } catch { 109 | NSLog(error.localizedDescription) 110 | } 111 | return [] 112 | } 113 | 114 | static func getSpOfCharWithLiuTab(_ chr: String) -> [String] { 115 | let request = NSFetchRequest(entityName: "Phrase") 116 | let keyPredicate = NSPredicate(format: "value == %@", chr) 117 | let spPredicate = NSPredicate(format: "sp == %@", NSNumber(value: true)) 118 | request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [keyPredicate, spPredicate]) 119 | do { 120 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 121 | return response.map { $0.key ?? "" } 122 | } catch { 123 | NSLog(error.localizedDescription) 124 | } 125 | return [] 126 | } 127 | 128 | static func getSpPhrasesForLiuTab(_ text: String) -> [Phrase] { 129 | let request = NSFetchRequest(entityName: "Phrase") 130 | let keyPredicate = NSPredicate(format: "key BEGINSWITH %@", text) 131 | let spPredicate = NSPredicate(format: "sp == %@", NSNumber(value: true)) 132 | request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [keyPredicate, spPredicate]) 133 | request.sortDescriptors = [NSSortDescriptor(key: "key_priority", ascending: true)] 134 | do { 135 | let response = try PersistenceController.shared.container.viewContext.fetch(request) 136 | return response 137 | } catch { 138 | NSLog(error.localizedDescription) 139 | } 140 | return [] 141 | } 142 | 143 | // MARK: Private 144 | 145 | // 快打模式下簡碼加選字碼可能和非簡碼有相同碼數 146 | // 使用.tab時不用特別處理,因使用.tab時簡碼判斷是直接讀字根檔 147 | // https://github.com/y1lichen/ilimi-inputmethod/issues/26 148 | private static func handleMultipleSp(_ text: String, _ isLoadByLiu: Bool) -> Bool { 149 | let sps: [String] = isLoadByLiu ? getSpOfCharWithLiuTab(text) : getSpOfCharWithoutLiuTab(text) 150 | if !sps.isEmpty { 151 | let minLen = sps.first!.count 152 | if LiuManager.shared.getPhraseExactly(sps.first!).first?.value != text, 153 | InputContext.shared.getCurrentInput().count == minLen + 1 { 154 | return true 155 | } 156 | } 157 | return false 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/Utils/ZhuyinMode/ZhuyinMode.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | 7 | extension IlimiInputController { 8 | // 把輸入碼轉成注音碼 9 | func getZhuyinMarkedText(_ text: String) -> String { 10 | "注" + StringConverter.shared.keyToZhuyins(text) 11 | } 12 | 13 | // 輸入';進入注音模式 14 | func checkIsZhuyinMode(_ input: String) -> Bool { 15 | let hadReadPinyin = UserDefaults.standard.object(forKey: "hadReadPinyinJson") as? Bool ?? false 16 | if !hadReadPinyin { 17 | // 沒有注音檔的話提示使用者 18 | NotifierController.notify(message: "請下載並匯入注音檔", stay: true) 19 | return false 20 | } 21 | isZhuyinMode = (input == "';") 22 | if isZhuyinMode { 23 | InputContext.shared.cleanUp() 24 | candidates.hide() 25 | client().setMarkedText("注", selectionRange: notFoundRange, replacementRange: notFoundRange) 26 | return true 27 | } 28 | return false 29 | } 30 | 31 | // 注音輸入的最後一碼是聲調 32 | func checkIsEndOfZhuyin(text: String) -> Bool { 33 | if (text.last?.isLetter) != nil { 34 | let num = Int(String(text.last!)) 35 | if num == 3 || num == 4 || num == 6 || num == 7 { 36 | return true 37 | } 38 | } 39 | return false 40 | } 41 | 42 | // 取得注音輸入的候選字 43 | func getNewCandidatesByZhuyin(comp: String, client sender: Any!) { 44 | if !comp.isEmpty { 45 | InputEngine.shared.getCadidatesByZhuyin(comp) 46 | if InputContext.shared.candidatesCount <= 0 { 47 | candidates.hide() 48 | return 49 | } 50 | candidates.update() 51 | candidates.show() 52 | ensureWindowLevel(client: sender) 53 | } else { 54 | candidates.hide() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/View/AddCustomPhrase/AddCustomPhraseSheetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/4/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct SheetView: View { 12 | // MARK: Lifecycle 13 | 14 | init(viewModel: CustomPhraseViewModel) { 15 | self.viewModel = viewModel 16 | } 17 | 18 | // MARK: Internal 19 | 20 | @ObservedObject var viewModel: CustomPhraseViewModel 21 | 22 | var body: some View { 23 | VStack { 24 | Text("新增自訂字詞") 25 | .font(.headline) 26 | .fontWeight(.heavy) 27 | Spacer() 28 | HStack { 29 | TextField("字碼", text: $viewModel.key) 30 | .frame(width: 80) 31 | TextField("字詞", text: $viewModel.value) 32 | } 33 | Spacer().frame(maxHeight: 20) 34 | HStack { 35 | Button("取消") { 36 | viewModel.showAddSheet = false 37 | viewModel.showEditSheet = false 38 | } 39 | Spacer() 40 | Button("完成") { 41 | if !viewModel.checkIsValid() { 42 | return 43 | } 44 | if viewModel.showAddSheet { 45 | viewModel.addCustomPhrase() 46 | viewModel.showAddSheet = false 47 | } else if viewModel.showEditSheet { 48 | viewModel.editCustomPhrase() 49 | viewModel.showEditSheet = false 50 | } 51 | } 52 | } 53 | } 54 | .frame(width: 300, height: 100) 55 | .padding() 56 | .onAppear { 57 | viewModel.syncKeyValueWithCustomPhraseToBeEdited() 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/View/AddCustomPhrase/AddCustomPhraseView.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Foundation 6 | import SwiftUI 7 | 8 | struct AddCustomPhraseView: View { 9 | @StateObject var viewModel = CustomPhraseViewModel() 10 | 11 | var body: some View { 12 | VStack { 13 | Table(of: CustomPhrase.self, selection: $viewModel.selected) { 14 | TableColumn("字碼") { 15 | Text($0.key ?? "") 16 | } 17 | TableColumn("字詞") { 18 | Text($0.value ?? "") 19 | } 20 | } 21 | rows: { 22 | ForEach(viewModel.customPhrases) { phrase in 23 | TableRow(phrase) 24 | .contextMenu { 25 | Button("Edit") { 26 | viewModel.openEditView(phrase) 27 | } 28 | Divider() 29 | Button("Delete", role: .destructive) { 30 | viewModel.delete(phrase) 31 | } 32 | } 33 | } 34 | } 35 | HStack { 36 | Spacer().frame(width: 5) 37 | Button("-") { 38 | for id in viewModel.selected { 39 | if let phrase = viewModel.customPhrases.first(where: { 40 | $0.id == id 41 | }) { 42 | viewModel.delete(phrase) 43 | } 44 | } 45 | } 46 | Button("+") { 47 | viewModel.showAddSheet = true 48 | } 49 | Spacer() 50 | }.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 0)) 51 | } 52 | .frame(width: 450, height: 250) 53 | .sheet(isPresented: $viewModel.showAddSheet) { 54 | SheetView(viewModel: viewModel) 55 | } 56 | .sheet(isPresented: $viewModel.showEditSheet) { 57 | SheetView(viewModel: viewModel) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/View/Menu/IlimiMenu.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | import Foundation 7 | 8 | extension Bool { 9 | fileprivate var state: NSControl.StateValue { 10 | self ? .on : .off 11 | } 12 | } 13 | 14 | extension IlimiInputController { 15 | @objc 16 | func reloadJson() { 17 | DataInitializer.shared.reloadAllData() 18 | } 19 | 20 | @objc 21 | func toggleTradToSim() { 22 | InputContext.shared.isTradToSim.toggle() 23 | } 24 | 25 | @objc 26 | func toggleSpMode() { 27 | InputContext.shared.isSpMode.toggle() 28 | } 29 | 30 | @objc 31 | func openDataFolder() { 32 | NSWorkspace.shared.open(DataInitializer.appSupportURL) 33 | } 34 | 35 | @objc 36 | func toggleGetZhuyinPanel() { 37 | (NSApp.delegate as? AppDelegate)?.showQueryWindow() 38 | } 39 | 40 | @objc 41 | func toggleSettingView() { 42 | (NSApp.delegate as? AppDelegate)?.showSettingsWindow() 43 | } 44 | 45 | @objc 46 | func toggleAddCustomPhraseView() { 47 | (NSApp.delegate as? AppDelegate)?.showSettingsWindow(1) 48 | } 49 | 50 | @objc 51 | func reloadApp() { 52 | NSApp.terminate(self) 53 | } 54 | 55 | // 在輸入法上的menu 56 | override public func menu() -> NSMenu! { 57 | let menu = NSMenu(title: "Ilimi Menu") 58 | let openDataFolderItem = NSMenuItem( 59 | title: "開啟使用者設定目錄", 60 | action: #selector(openDataFolder), 61 | keyEquivalent: "" 62 | ) 63 | let reloadJsonItem = NSMenuItem( 64 | title: "匯入字檔", 65 | action: #selector(reloadJson), 66 | keyEquivalent: "" 67 | ) 68 | let getZhuyinItem = NSMenuItem( 69 | title: "反查注音/查碼(,,q)", 70 | action: #selector(toggleGetZhuyinPanel), 71 | keyEquivalent: "" 72 | ) 73 | let toggleTradToSimItem = NSMenuItem( 74 | title: "打繁出簡模式(,,ct)", 75 | action: #selector(toggleTradToSim), 76 | keyEquivalent: "" 77 | ) 78 | let toggleSpModelItem = NSMenuItem( 79 | title: "快打模式(,,sp)", 80 | action: #selector(toggleSpMode), 81 | keyEquivalent: "" 82 | ) 83 | let openSettingItem = NSMenuItem( 84 | title: "設定", 85 | action: #selector(toggleSettingView), 86 | keyEquivalent: "" 87 | ) 88 | let addCustomPhraseItem = NSMenuItem( 89 | title: "自定義加詞", 90 | action: #selector(toggleAddCustomPhraseView), 91 | keyEquivalent: "" 92 | ) 93 | let reloadAppItem = NSMenuItem( 94 | title: "重啟輸入法", 95 | action: #selector(reloadApp), 96 | keyEquivalent: "" 97 | ) 98 | // 開啟打繁出簡模式後,在MenuItem上顯示勾符號 99 | toggleTradToSimItem.state = InputContext.shared.isTradToSim.state 100 | toggleSpModelItem.state = InputContext.shared.isSpMode.state 101 | menu.items = [ 102 | getZhuyinItem, 103 | toggleTradToSimItem, 104 | toggleSpModelItem, 105 | NSMenuItem.separator(), 106 | openSettingItem, 107 | addCustomPhraseItem, 108 | openDataFolderItem, 109 | reloadJsonItem, 110 | NSMenuItem.separator(), 111 | reloadAppItem, 112 | ] 113 | return menu 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/View/Menu/MainMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/3/27. 6 | // 7 | 8 | import AppKit 9 | import Foundation 10 | 11 | class MainMenu: NSMenu { 12 | override init(title: String) { 13 | super.init(title: title) 14 | let fileMenu = NSMenuItem() 15 | fileMenu.submenu = NSMenu(title: "File") 16 | // cmd+關閉視窗 17 | fileMenu.submenu?.items = [ 18 | NSMenuItem( 19 | title: "Close window", 20 | action: #selector(NSApplication.shared.keyWindow?.close), 21 | keyEquivalent: "w" 22 | ), 23 | ] 24 | let editMenu = NSMenuItem() 25 | editMenu.submenu = NSMenu(title: "Edit") 26 | // 剪貼板熱鍵 27 | editMenu.submenu?.items = [ 28 | NSMenuItem(title: "Undo", action: #selector(UndoManager.undo), keyEquivalent: "z"), 29 | NSMenuItem(title: "Redo", action: #selector(UndoManager.redo), keyEquivalent: "Z"), 30 | NSMenuItem.separator(), 31 | NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"), 32 | NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"), 33 | NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"), 34 | NSMenuItem.separator(), 35 | NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"), 36 | NSMenuItem.separator(), 37 | NSMenuItem(title: "Duplicate", action: #selector(NSApplication.copy), keyEquivalent: "d"), 38 | ] 39 | items = [fileMenu, editMenu] 40 | } 41 | 42 | required init(coder: NSCoder) { 43 | super.init(coder: coder) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/View/Notifier/Notifier.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Cocoa 6 | 7 | extension NSRect { 8 | public static var Zero: NSRect { 9 | NSRect(x: 0.0, y: 0.0, width: 0.114, height: 0.514) 10 | } 11 | } 12 | 13 | // MARK: - NotifierWindowDelegate 14 | 15 | private protocol NotifierWindowDelegate: AnyObject { 16 | func windowDidBecomeClicked(_ window: NotifierWindow) 17 | } 18 | 19 | // MARK: - NotifierWindow 20 | 21 | private class NotifierWindow: NSWindow { 22 | weak var clickDelegate: NotifierWindowDelegate? 23 | 24 | override func mouseDown(with _: NSEvent) { 25 | clickDelegate?.windowDidBecomeClicked(self) 26 | } 27 | } 28 | 29 | private let kWindowWidth: CGFloat = 213.0 30 | private let kWindowHeight: CGFloat = 60.0 31 | 32 | // MARK: - NotifierController 33 | 34 | public class NotifierController: NSWindowController, NotifierWindowDelegate { 35 | // MARK: Lifecycle 36 | 37 | private init() { 38 | let screenRect = NSScreen.main?.visibleFrame ?? NSRect.Zero 39 | let contentRect = NSRect(x: 0, y: 0, width: kWindowWidth, height: kWindowHeight) 40 | var windowRect = contentRect 41 | windowRect.origin.x = screenRect.maxX - windowRect.width - 10 42 | windowRect.origin.y = screenRect.maxY - windowRect.height - 10 43 | let styleMask: NSWindow.StyleMask = [.fullSizeContentView, .titled] 44 | 45 | let transparentVisualEffect = NSVisualEffectView() 46 | transparentVisualEffect.blendingMode = .behindWindow 47 | transparentVisualEffect.state = .active 48 | 49 | let panel = NotifierWindow( 50 | contentRect: windowRect, styleMask: styleMask, backing: .buffered, defer: false 51 | ) 52 | panel.contentView = transparentVisualEffect 53 | panel.isMovableByWindowBackground = true 54 | panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel)) 55 | panel.hasShadow = true 56 | panel.backgroundColor = backgroundColor 57 | panel.title = "" 58 | panel.titlebarAppearsTransparent = true 59 | panel.titleVisibility = .hidden 60 | panel.showsToolbarButton = false 61 | panel.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true 62 | panel.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true 63 | panel.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true 64 | panel.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true 65 | 66 | self.messageTextField = NSTextField() 67 | messageTextField.frame = contentRect 68 | messageTextField.isEditable = false 69 | messageTextField.isSelectable = false 70 | messageTextField.isBezeled = false 71 | messageTextField.textColor = foregroundColor 72 | messageTextField.drawsBackground = false 73 | messageTextField.font = .boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)) 74 | panel.contentView?.addSubview(messageTextField) 75 | 76 | super.init(window: panel) 77 | 78 | panel.clickDelegate = self 79 | } 80 | 81 | @available(*, unavailable) 82 | required init?(coder _: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | 86 | // MARK: Public 87 | 88 | public static func notify(message: String, stay: Bool = false) { 89 | let controller = NotifierController() 90 | controller.message = message 91 | controller.shouldStay = stay 92 | controller.show() 93 | } 94 | 95 | override public func close() { 96 | waitTimer?.invalidate() 97 | waitTimer = nil 98 | fadeTimer?.invalidate() 99 | fadeTimer = nil 100 | super.close() 101 | } 102 | 103 | // MARK: Fileprivate 104 | 105 | fileprivate func windowDidBecomeClicked(_: NotifierWindow) { 106 | fadeOut() 107 | } 108 | 109 | // MARK: Private 110 | 111 | private static var instanceCount = 0 112 | private static var lastLocation = NSPoint.zero 113 | 114 | private var messageTextField: NSTextField 115 | 116 | private var shouldStay = false 117 | private var waitTimer: Timer? 118 | private var fadeTimer: Timer? 119 | 120 | private var message: String = "" { 121 | didSet { 122 | let paraStyle = NSMutableParagraphStyle() 123 | paraStyle.setParagraphStyle(NSParagraphStyle.default) 124 | paraStyle.alignment = .center 125 | let attr: [NSAttributedString.Key: AnyObject] = [ 126 | .foregroundColor: foregroundColor, 127 | .font: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)), 128 | .paragraphStyle: paraStyle, 129 | ] 130 | let attrString = NSAttributedString(string: message, attributes: attr) 131 | messageTextField.attributedStringValue = attrString 132 | let width = window?.frame.width ?? kWindowWidth 133 | let rect = attrString.boundingRect( 134 | with: NSSize(width: width, height: 1_600), options: .usesLineFragmentOrigin 135 | ) 136 | let height = rect.height 137 | let x = messageTextField.frame.origin.x 138 | let y = ((window?.frame.height ?? kWindowHeight) - height) / 2 139 | let newFrame = NSRect(x: x, y: y, width: width, height: height) 140 | messageTextField.frame = newFrame 141 | } 142 | } 143 | 144 | private var backgroundColor: NSColor = .textBackgroundColor { 145 | didSet { 146 | window?.backgroundColor = backgroundColor 147 | } 148 | } 149 | 150 | private var foregroundColor: NSColor = .controlTextColor { 151 | didSet { 152 | messageTextField.textColor = foregroundColor 153 | } 154 | } 155 | 156 | private static func increaseInstanceCount() { 157 | instanceCount += 1 158 | } 159 | 160 | private static func decreaseInstanceCount() { 161 | instanceCount -= 1 162 | if instanceCount < 0 { 163 | instanceCount = 0 164 | } 165 | } 166 | 167 | private func show() { 168 | func setStartLocation() { 169 | if Self.instanceCount == 0 { 170 | return 171 | } 172 | let lastLocation = Self.lastLocation 173 | let screenRect = NSScreen.main?.visibleFrame ?? NSRect.Zero 174 | var windowRect = window?.frame ?? NSRect.Zero 175 | windowRect.origin.x = lastLocation.x 176 | windowRect.origin.y = lastLocation.y - 10 - windowRect.height 177 | 178 | if windowRect.origin.y < screenRect.minY { 179 | return 180 | } 181 | 182 | window?.setFrame(windowRect, display: true) 183 | } 184 | 185 | func moveIn() { 186 | let afterRect = window?.frame ?? NSRect.Zero 187 | Self.lastLocation = afterRect.origin 188 | var beforeRect = afterRect 189 | beforeRect.origin.y += 10 190 | window?.setFrame(beforeRect, display: true) 191 | window?.orderFront(self) 192 | window?.setFrame(afterRect, display: true, animate: true) 193 | } 194 | 195 | setStartLocation() 196 | moveIn() 197 | Self.increaseInstanceCount() 198 | waitTimer = Timer.scheduledTimer( 199 | timeInterval: shouldStay ? 5 : 1, target: self, selector: #selector(fadeOut), 200 | userInfo: nil, 201 | repeats: false 202 | ) 203 | } 204 | 205 | @objc 206 | private func doFadeOut(_: Timer) { 207 | let opacity = window?.alphaValue ?? 0 208 | if opacity <= 0 { 209 | close() 210 | } else { 211 | window?.alphaValue = opacity - 0.2 212 | } 213 | } 214 | 215 | @objc 216 | private func fadeOut() { 217 | waitTimer?.invalidate() 218 | waitTimer = nil 219 | Self.decreaseInstanceCount() 220 | fadeTimer = Timer.scheduledTimer( 221 | timeInterval: 0.01, target: self, selector: #selector(doFadeOut(_:)), userInfo: nil, 222 | repeats: true 223 | ) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/View/Query/QueryView.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import SwiftUI 6 | 7 | // MARK: - QueryResult 8 | 9 | struct QueryResult: Identifiable { 10 | let id = UUID() 11 | 12 | var char: String 13 | var zhuyin: String 14 | var inputCode: String 15 | } 16 | 17 | // MARK: - QueryView 18 | 19 | struct QueryView: View { 20 | @State var textFieldText: String = "" 21 | @State var results: [QueryResult] = [] 22 | 23 | var body: some View { 24 | VStack { 25 | TextField("輸入字詞", text: $textFieldText, onCommit: onCommit) 26 | .textFieldStyle(.plain) 27 | .padding() 28 | .frame(width: 380) 29 | Spacer() 30 | Table(results) { 31 | TableColumn("文字", value: \.char) 32 | .width(60) 33 | TableColumn("輸入碼", value: \.inputCode) 34 | TableColumn("注音", value: \.zhuyin) 35 | } 36 | }.padding() 37 | } 38 | 39 | func onCommit() { 40 | results = [] 41 | var temp = [QueryResult]() 42 | for i in 0 ..< textFieldText.count { 43 | temp.append(handler(textFieldText[i])) 44 | } 45 | results = temp 46 | } 47 | 48 | func handler(_ text: String) -> QueryResult { 49 | var res = QueryResult(char: text, zhuyin: "", inputCode: "") 50 | res.inputCode = CoreDataHelper.getKeyOfChar(text).joined(separator: " ") 51 | res.zhuyin = CoreDataHelper.getZhuyinOfChar(text).joined(separator: " ") 52 | return res 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/View/Settings/GeneralSettingsView.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Foundation 6 | import SwiftUI 7 | 8 | struct GeneralSettingsView: View { 9 | @StateObject var settingViewModel = SettingViewModel() 10 | 11 | var body: some View { 12 | ScrollView(showsIndicators: false) { 13 | VStack(alignment: .leading, spacing: 20) { 14 | Picker("字體大小", selection: $settingViewModel.fontSize) { 15 | fontPickerContent() 16 | }.onChange(of: settingViewModel.fontSize) { _ in 17 | settingViewModel.killApplicationToReload() 18 | } 19 | Picker("選字窗排列", selection: $settingViewModel.isHorizontalCandidatesPanel) { 20 | Text("橫式").tag(true) 21 | Text("直式").tag(false) 22 | } 23 | .onChange(of: settingViewModel.isHorizontalCandidatesPanel) { _ in 24 | settingViewModel.killApplicationToReload() 25 | } 26 | .pickerStyle(RadioGroupPickerStyle()) 27 | Picker("只顯示完全匹配字碼的候選字", selection: $settingViewModel.showOnlyExactlyMatch) { 28 | Text("是").tag(true) 29 | Text("否").tag(false) 30 | } 31 | .pickerStyle(RadioGroupPickerStyle()) 32 | .horizontalRadioGroupLayout() 33 | Picker("在沒有候選字時限制輸入", selection: $settingViewModel.limitInputWhenNoCandidate) { 34 | Text("是").tag(true) 35 | Text("否").tag(false) 36 | } 37 | .pickerStyle(RadioGroupPickerStyle()) 38 | .horizontalRadioGroupLayout() 39 | Picker("使用注音輸入後提示拆碼", selection: $settingViewModel.showLiuKeyAfterZhuyin) { 40 | Text("是").tag(true) 41 | Text("否").tag(false) 42 | } 43 | .pickerStyle(RadioGroupPickerStyle()) 44 | .horizontalRadioGroupLayout() 45 | Picker("選字碼", selection: $settingViewModel.selectCandidateBy1to8) { 46 | Text("1到9").tag(true) 47 | Text("0到8").tag(false) 48 | } 49 | .onChange(of: settingViewModel.selectCandidateBy1to8) { _ in 50 | settingViewModel.killApplicationToReload() 51 | } 52 | .pickerStyle(RadioGroupPickerStyle()) 53 | .horizontalRadioGroupLayout() 54 | Picker("靜音模式", selection: $settingViewModel.silentMode) { 55 | Text("是").tag(true) 56 | Text("否").tag(false) 57 | } 58 | .pickerStyle(RadioGroupPickerStyle()) 59 | .horizontalRadioGroupLayout() 60 | Picker("自動檢查更新", selection: $settingViewModel.autoCheckUpdate) { 61 | Text("是").tag(true) 62 | Text("否").tag(false) 63 | } 64 | .pickerStyle(RadioGroupPickerStyle()) 65 | .horizontalRadioGroupLayout() 66 | Button { 67 | UpdateManager.checkUpdate(isManual: true) 68 | } label: { 69 | Text("檢查更新") 70 | } 71 | } 72 | .frame(width: 250) 73 | .padding() 74 | } 75 | .frame(width: 450, height: 250) 76 | } 77 | 78 | @ViewBuilder 79 | func fontPickerContent() -> some View { 80 | ForEach(settingViewModel.fontSizeValues, id: \.self) { 81 | Text("\($0)") 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/View/Settings/SettingsViewToolbar.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import Foundation 6 | import SwiftUI 7 | 8 | extension NSToolbarItem.Identifier { 9 | static let general = NSToolbarItem.Identifier(rawValue: "general") 10 | static let addCustomPhrase = NSToolbarItem.Identifier(rawValue: "addCustomPhrase") 11 | } 12 | 13 | extension NSToolbar { 14 | static let settingsViewToolBar: NSToolbar = { 15 | let toolbar = NSToolbar(identifier: "SettingsViewToolbar") 16 | toolbar.displayMode = .iconAndLabel 17 | return toolbar 18 | }() 19 | } 20 | 21 | // MARK: - AppDelegate + NSToolbarDelegate 22 | 23 | extension AppDelegate: NSToolbarDelegate { 24 | @objc 25 | func openSettingView() { 26 | (NSApp.delegate as? AppDelegate)?.showSettingsWindow() 27 | } 28 | 29 | @objc 30 | func openAddCustomPhraseView() { 31 | (NSApp.delegate as? AppDelegate)?.showSettingsWindow(1) 32 | } 33 | 34 | public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 35 | [.general, .addCustomPhrase] 36 | } 37 | 38 | public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 39 | [.general, .addCustomPhrase] 40 | } 41 | 42 | public func toolbar( 43 | _ toolbar: NSToolbar, 44 | itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, 45 | willBeInsertedIntoToolbar flag: Bool 46 | ) 47 | -> NSToolbarItem? { 48 | switch itemIdentifier { 49 | case .general: 50 | let item = NSToolbarItem(itemIdentifier: itemIdentifier) 51 | item.label = "一般" 52 | let button = NSButton( 53 | image: NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)!, 54 | target: nil, 55 | action: #selector(openSettingView) 56 | ) 57 | button.bezelStyle = .recessed 58 | item.view = button 59 | return item 60 | 61 | case .addCustomPhrase: 62 | let item = NSToolbarItem(itemIdentifier: itemIdentifier) 63 | item.label = "自定義加詞" 64 | let button = NSButton( 65 | image: NSImage(systemSymbolName: "pencil", accessibilityDescription: nil)!, 66 | target: nil, 67 | action: #selector(openAddCustomPhraseView) 68 | ) 69 | button.bezelStyle = .recessed 70 | item.view = button 71 | return item 72 | 73 | default: 74 | return nil 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/ViewModel/CustomPhraseViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/4/24. 6 | // 7 | 8 | import Foundation 9 | 10 | class CustomPhraseViewModel: ObservableObject { 11 | // MARK: Lifecycle 12 | 13 | init() { 14 | fetchData() 15 | } 16 | 17 | // MARK: Internal 18 | 19 | @Published var key: String = "" 20 | @Published var value: String = "" 21 | 22 | @Published var showEditSheet = false 23 | 24 | @Published var showAddSheet = false 25 | var selected = Set() 26 | 27 | var customPhraseToBeEdited: CustomPhrase? 28 | 29 | // static var shared = CustomPhraseViewModel() 30 | 31 | @Published var customPhrases: [CustomPhrase] = [] 32 | 33 | func fetchData() { 34 | customPhrases = CustomPhraseManager.getAllCustomPhrase() 35 | } 36 | 37 | func delete(_ customPhrase: CustomPhrase) { 38 | CustomPhraseManager.deleteCustomPhrase(customPhrase) 39 | fetchData() 40 | } 41 | 42 | func addCustomPhrase() { 43 | CustomPhraseManager.addCustomPhrase(key: key, value: value) 44 | fetchData() 45 | clearKeyValue() 46 | } 47 | 48 | func openEditView(_ customPhrase: CustomPhrase) { 49 | showEditSheet = true 50 | customPhraseToBeEdited = customPhrase 51 | } 52 | 53 | func syncKeyValueWithCustomPhraseToBeEdited() { 54 | if showEditSheet { 55 | if let customPhraseToBeEdited = customPhraseToBeEdited { 56 | key = customPhraseToBeEdited.key ?? "" 57 | value = customPhraseToBeEdited.value ?? "" 58 | } 59 | } 60 | } 61 | 62 | func editCustomPhrase() { 63 | if let customPhraseToBeEdited = customPhraseToBeEdited { 64 | CustomPhraseManager.editCustomPhrase(customPhraseToBeEdited, key: key, value: value) 65 | clearKeyValue() 66 | } 67 | } 68 | 69 | func checkIsValid() -> Bool { 70 | if key.count > 5 { 71 | NotifierController.notify(message: "自訂字詞的字碼以5碼為上限") 72 | return false 73 | } 74 | return true 75 | } 76 | 77 | // MARK: Private 78 | 79 | private func clearKeyValue() { 80 | key = "" 81 | value = "" 82 | customPhraseToBeEdited = nil 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Sources/ilimiMainAssembly/ViewModel/SettingViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by 陳奕利 on 2024/5/1. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class SettingViewModel: ObservableObject { 12 | static let shared = SettingViewModel() 13 | 14 | // 預設字體大小 22 15 | @AppStorage("fontSize") var fontSize = 22 16 | // 預設橫排選字窗 17 | @AppStorage("isHorizontalCandidatesPanel") var isHorizontalCandidatesPanel = true 18 | // 預設不在沒有候選字時限制輸入 19 | @AppStorage("limitInputWhenNoCandidate") var limitInputWhenNoCandidate = false 20 | // 預設只顯示完全匹配字碼的字元 21 | @AppStorage("showOnlyExactlyMatch") var showOnlyExactlyMatch = true 22 | // 預設使用注音輸入後提示拆碼 23 | @AppStorage("showLiuKeyAfterZhuyin") var showLiuKeyAfterZhuyin = true 24 | // 預設使用1-9選字 25 | @AppStorage("selectCandidateBy1to8") var selectCandidateBy1to8 = true 26 | // 靜音模式 27 | @AppStorage("silentMode") var silentMode = false 28 | // 自動檢查 29 | @AppStorage("autoCheckUpdate") var autoCheckUpdate = true 30 | 31 | let fontSizeValues = [14, 16, 18, 20, 22, 24, 28, 32] 32 | 33 | func killApplicationToReload() { 34 | NSApp.terminate(self) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Tests/ilimiMainAssemblyTests/ComponentsForTests/vChewing/KeyCodeMapForTests.swift: -------------------------------------------------------------------------------- 1 | // (c) 2021 and onwards The vChewing Project (MIT-NTL License). 2 | // ==================== 3 | // This code is released under the MIT license (SPDX-License-Identifier: MIT) 4 | // ... with NTL restriction stating that: 5 | // No trademark license is granted to use the trade names, trademarks, service 6 | // marks, or product names of Contributor, except as required to fulfill notice 7 | // requirements defined in MIT License. 8 | 9 | import Foundation 10 | 11 | let mapKeyCodesANSIForTests: [String: UInt16] = [ 12 | "1": 18, "2": 19, "3": 20, "4": 21, "5": 23, "6": 22, "7": 26, "8": 28, "9": 25, "0": 29, "-": 27, 13 | "=": 24, "q": 12, "w": 13, "e": 14, "r": 15, "t": 17, "y": 16, "u": 32, "i": 34, "o": 31, "p": 35, 14 | "[": 33, "]": 30, "\\": 42, "a": 0, "s": 1, "d": 2, "f": 3, "g": 5, "h": 4, "j": 38, "k": 40, 15 | "l": 37, ";": 41, "'": 39, "z": 6, "x": 7, "c": 8, "v": 9, "b": 11, "n": 45, "m": 46, ",": 43, 16 | ".": 47, "/": 44, 17 | ] 18 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Tests/ilimiMainAssemblyTests/ComponentsForTests/vChewing/MockedClient.swift: -------------------------------------------------------------------------------- 1 | // (c) 2021 and onwards The vChewing Project (MIT-NTL License). 2 | // ==================== 3 | // This code is released under the MIT license (SPDX-License-Identifier: MIT) 4 | // ... with NTL restriction stating that: 5 | // No trademark license is granted to use the trade names, trademarks, service 6 | // marks, or product names of Contributor, except as required to fulfill notice 7 | // requirements defined in MIT License. 8 | 9 | import InputMethodKit 10 | 11 | class FakeClient: NSObject, IMKTextInput { 12 | var attributedString: NSMutableAttributedString = .init(string: "") 13 | var selectedRangeStored: NSRange = .notFound 14 | var markedRangeStored: NSRange = .notFound 15 | var markedText: NSAttributedString = .init(string: "") 16 | 17 | var cursor = 0 { 18 | didSet { 19 | cursor = max(0, min(cursor, attributedString.length)) 20 | } 21 | } 22 | 23 | func toString() -> String { 24 | attributedString.string 25 | } 26 | 27 | func clear() { 28 | cursor = 0 29 | attributedString = .init() 30 | } 31 | 32 | func insertText(_ string: Any!, replacementRange: NSRange) { 33 | guard let string = string as? String else { return } 34 | var insertionPoint = replacementRange.location 35 | if insertionPoint == NSNotFound { 36 | insertionPoint = cursor 37 | } 38 | cursor = insertionPoint 39 | attributedString.insert(.init(string: string), at: cursor) 40 | cursor += string.utf16.count 41 | } 42 | 43 | func setMarkedText(_ string: Any!, selectionRange _: NSRange, replacementRange: NSRange) { 44 | markedText = string as? NSAttributedString ?? .init(string: string as? String ?? "") 45 | var insertionPoint = replacementRange.location 46 | if insertionPoint == NSNotFound { 47 | insertionPoint = cursor 48 | } 49 | cursor = insertionPoint 50 | } 51 | 52 | func selectedRange() -> NSRange { 53 | NSIntersectionRange(selectedRangeStored, .init(location: 0, length: attributedString.length)) 54 | } 55 | 56 | func markedRange() -> NSRange { 57 | NSIntersectionRange(markedRangeStored, .init(location: 0, length: attributedString.length)) 58 | } 59 | 60 | func attributedSubstring(from range: NSRange) -> NSAttributedString! { 61 | let usableRange = NSIntersectionRange(range, .init(location: 0, length: attributedString.length)) 62 | return attributedString.attributedSubstring(from: usableRange) 63 | } 64 | 65 | func length() -> Int { 66 | attributedString.length 67 | } 68 | 69 | func characterIndex( 70 | for _: NSPoint, 71 | tracking _: IMKLocationToOffsetMappingMode, 72 | inMarkedRange _: UnsafeMutablePointer! 73 | ) 74 | -> Int { 75 | cursor 76 | } 77 | 78 | func attributes( 79 | forCharacterIndex _: Int, 80 | lineHeightRectangle _: UnsafeMutablePointer! 81 | ) 82 | -> [AnyHashable: Any]! { 83 | [:] 84 | } 85 | 86 | func validAttributesForMarkedText() -> [Any]! { 87 | [] 88 | } 89 | 90 | func overrideKeyboard(withKeyboardNamed keyboardUniqueName: String!) { 91 | _ = keyboardUniqueName 92 | } 93 | 94 | func selectMode(_ modeIdentifier: String!) { 95 | _ = modeIdentifier 96 | } 97 | 98 | func supportsUnicode() -> Bool { 99 | true 100 | } 101 | 102 | func bundleIdentifier() -> String { 103 | "com.jefferson.ilimi.MainAssembly.UnitTests.MockedClient" 104 | } 105 | 106 | func windowLevel() -> CGWindowLevel { 107 | CGShieldingWindowLevel() 108 | } 109 | 110 | func supportsProperty(_: TSMDocumentPropertyTag) -> Bool { 111 | false 112 | } 113 | 114 | func uniqueClientIdentifierString() -> String { 115 | bundleIdentifier() 116 | } 117 | 118 | func string(from range: NSRange, actualRange: NSRangePointer!) -> String! { 119 | let actualNSRange = actualRange.move() 120 | var usableRange = NSIntersectionRange(actualNSRange, range) 121 | usableRange = NSIntersectionRange(usableRange, .init(location: 0, length: attributedString.length)) 122 | return attributedString.attributedSubstring(from: usableRange).string 123 | } 124 | 125 | func firstRect(forCharacterRange _: NSRange, actualRange _: NSRangePointer!) -> NSRect { 126 | .zero 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Tests/ilimiMainAssemblyTests/ComponentsForTests/vChewing/NSEventImplForTests.swift: -------------------------------------------------------------------------------- 1 | // (c) 2021 and onwards The vChewing Project (MIT-NTL License). 2 | // ==================== 3 | // This code is released under the MIT license (SPDX-License-Identifier: MIT) 4 | // ... with NTL restriction stating that: 5 | // No trademark license is granted to use the trade names, trademarks, service 6 | // marks, or product names of Contributor, except as required to fulfill notice 7 | // requirements defined in MIT License. 8 | 9 | import AppKit 10 | 11 | extension NSEvent { 12 | public struct KeyEventData { 13 | // MARK: Lifecycle 14 | 15 | public init( 16 | type: EventType = .keyDown, 17 | flags: ModifierFlags = [], 18 | chars: String, 19 | charsSansModifiers: String? = nil, 20 | keyCode: UInt16? = nil 21 | ) { 22 | self.type = type 23 | self.flags = flags 24 | self.chars = chars 25 | self.charsSansModifiers = charsSansModifiers ?? chars 26 | self.keyCode = keyCode ?? mapKeyCodesANSIForTests[chars] ?? 65_535 27 | } 28 | 29 | // MARK: Public 30 | 31 | public var type: EventType = .keyDown 32 | public var flags: ModifierFlags 33 | public var chars: String 34 | public var charsSansModifiers: String 35 | public var keyCode: UInt16 36 | 37 | public var asPairedEvents: [NSEvent] { 38 | NSEvent.keyEvents(data: self, paired: true) 39 | } 40 | 41 | public var asEvent: NSEvent? { 42 | NSEvent.keyEvent(data: self) 43 | } 44 | 45 | public func toEvents(paired: Bool = false) -> [NSEvent] { 46 | NSEvent.keyEvents(data: self, paired: paired) 47 | } 48 | } 49 | 50 | public static func keyEvents(data: KeyEventData, paired: Bool = false) -> [NSEvent] { 51 | var resultArray = [NSEvent]() 52 | if let eventA: NSEvent = Self.keyEvent(data: data) { 53 | resultArray.append(eventA) 54 | if paired, eventA.type == .keyDown, 55 | let eventB = eventA.reinitiate(with: .keyUp, characters: nil, charactersIgnoringModifiers: nil) { 56 | resultArray.append(eventB) 57 | } 58 | } 59 | return resultArray 60 | } 61 | 62 | public static func keyEvent(data: KeyEventData) -> NSEvent? { 63 | Self.keyEventSimple( 64 | type: data.type, 65 | flags: data.flags, 66 | chars: data.chars, 67 | charsSansModifiers: data.charsSansModifiers, 68 | keyCode: data.keyCode 69 | ) 70 | } 71 | 72 | public static func keyEventSimple( 73 | type: EventType, 74 | flags: ModifierFlags, 75 | chars: String, 76 | charsSansModifiers: String? = nil, 77 | keyCode: UInt16 78 | ) 79 | -> NSEvent? { 80 | Self.keyEvent( 81 | with: type, location: .zero, modifierFlags: flags, timestamp: .init(), 82 | windowNumber: 0, context: nil, characters: chars, 83 | charactersIgnoringModifiers: charsSansModifiers ?? chars, isARepeat: false, keyCode: keyCode 84 | ) 85 | } 86 | } 87 | 88 | // MARK: - NSRange Extension 89 | 90 | extension NSRange { 91 | public static var zero = NSRange(location: 0, length: 0) 92 | public static var notFound = NSRange(location: NSNotFound, length: NSNotFound) 93 | } 94 | -------------------------------------------------------------------------------- /Packages/ilimi_MainAssembly/Tests/ilimiMainAssemblyTests/ilimiMainAssemblyTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ilimiMainAssembly 2 | import XCTest 3 | 4 | final class ilimiMainAssemblyTests: XCTestCase { 5 | func testGetSpOfCharWithoutLiuTab() throws { 6 | DataInitializer.shared.initDataWhenStart() 7 | print(SpModeManager.getSpOfCharWithoutLiuTab("嘸") as Any) 8 | print(SpModeManager.getSpOfCharWithoutLiuTab("蝦") as Any) 9 | } 10 | 11 | func testConvertLiuTab() throws { 12 | LiuUniTabConverter().convertLiuUniTab() 13 | let sharedEngine = InputEngine.shared 14 | let sharedInputContext = InputContext.shared 15 | sharedEngine.getCandidates("dez") 16 | XCTAssertNotEqual(0, sharedInputContext.candidates.count) 17 | print(sharedInputContext.candidates) 18 | } 19 | 20 | func testDataLoadAndQuery() throws { 21 | DataInitializer.shared.initDataWhenStart() 22 | DataInitializer.shared.loadLiuData() 23 | DataInitializer.shared.loadPinyinJson() 24 | let sharedEngine = InputEngine.shared 25 | let sharedInputContext = InputContext.shared 26 | // sharedEngine.getCandidates(",]]") 27 | // sharedEngine.getCandidates("ix") 28 | // XCTAssertNotEqual(0, sharedInputContext.candidates.count) 29 | // print(sharedInputContext.candidates) 30 | // print("Found \(sharedInputContext.candidates.count) candidates.") 31 | sharedEngine.getCandidates("dez") 32 | XCTAssertNotEqual(0, sharedInputContext.candidates.count) 33 | print(sharedInputContext.candidates) 34 | } 35 | 36 | // 測試同音輸入 37 | func testFindCharWithSamePronunciation() throws { 38 | DataInitializer.shared.initDataWhenStart() 39 | DataInitializer.shared.loadLiuData() 40 | DataInitializer.shared.loadPinyinJson() 41 | let res = CoreDataHelper.getCharWithSamePronunciation("我") 42 | XCTAssertNotEqual(0, res.count) 43 | print("Found \(res.count) candidates.") 44 | } 45 | 46 | func testReadCin() throws { 47 | let sharedEngine = InputEngine.shared 48 | let sharedInputContext = InputContext.shared 49 | let reader = CinReader() 50 | reader.readCin() 51 | sharedEngine.getCandidates("dez") 52 | XCTAssertNotEqual(0, sharedInputContext.candidates.count) 53 | print(sharedInputContext.candidates) 54 | } 55 | 56 | func testCheckUpdate() throws { 57 | UpdateManager.checkUpdate() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Packages/vChewing_IMKUtils_IlimiImpl/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Packages/vChewing_IMKUtils_IlimiImpl/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "vChewing_IMKUtils_IlimiImpl", 7 | platforms: [ 8 | .macOS(.v13), 9 | ], 10 | products: [ 11 | .library( 12 | name: "vChewing_IMKUtils_IlimiImpl", 13 | targets: ["IMKUtils"] 14 | ), 15 | ], 16 | dependencies: [], 17 | targets: [ 18 | .target( 19 | name: "IMKUtils", 20 | dependencies: [] 21 | ), 22 | .testTarget( 23 | name: "IMKUtilsTests", 24 | dependencies: ["IMKUtils"] 25 | ), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Packages/vChewing_IMKUtils_IlimiImpl/README.md: -------------------------------------------------------------------------------- 1 | # IMKUtils 2 | 3 | (該模組取自「威注音輸入法」,且刪掉了部分「一粒米輸入法」用不到的內容。) 4 | 5 | 該模組套裝使得輸入法的登記啟用過程與鍵盤佈局管理過程更加簡便。分兩部分組成: 6 | 7 | - IMKHelper: (c) 2021 and onwards The vChewing Project (MIT-NTL License). 8 | - TISInputSourceExtension: 9 | - (c) 2021 and onwards The vChewing Project (MIT-NTL License). 10 | - (c) 2018 and onwards Mizuno Hiroki (a.k.a. "Mzp") (MIT License). 11 | - 詳情請參見 TISInputSourceExtension.swift 內部的各種 pragma mark 標記。 12 | -------------------------------------------------------------------------------- /Packages/vChewing_IMKUtils_IlimiImpl/Sources/IMKUtils/IMKHelper.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The vChewing Project (MIT-NTL License). 2 | // ==================== 3 | // This code is released under the MIT license (SPDX-License-Identifier: MIT) 4 | // ... with NTL restriction stating that: 5 | // No trademark license is granted to use the trade names, trademarks, service 6 | // marks, or product names of Contributor, except as required to fulfill notice 7 | // requirements defined in MIT License. 8 | 9 | import Foundation 10 | import InputMethodKit 11 | 12 | // MARK: - IMKHelper 13 | 14 | public enum IMKHelper { 15 | public struct CarbonKeyboardLayout { 16 | var strName: String = "" 17 | var strValue: String = "" 18 | } 19 | 20 | /// 威注音有專門統計過,實際上會有差異的英數鍵盤佈局只有這幾種。 21 | /// 精簡成這種清單的話,不但節省 SwiftUI 的繪製壓力,也方便使用者做選擇。 22 | public static let arrWhitelistedKeyLayoutsASCII: [String] = { 23 | var results = LatinKeyboardMappings.allCases 24 | if #available(macOS 10.13, *) { 25 | results = results.filter { 26 | ![.qwertyUS, .qwertzGerman, .azertyFrench].contains($0) 27 | } 28 | } 29 | return results.map(\.rawValue) 30 | }() 31 | 32 | public static let arrDynamicBasicKeyLayouts: [String] = [ 33 | "com.apple.keylayout.ZhuyinBopomofo", 34 | "com.apple.keylayout.ZhuyinEten", 35 | ] 36 | 37 | public static var allowedAlphanumericalTISInputSources: [TISInputSource.KeyboardLayout] { 38 | let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap() 39 | return arrWhitelistedKeyLayoutsASCII.compactMap { allTISKeyboardLayouts[$0] } 40 | } 41 | 42 | public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource.KeyboardLayout?] { 43 | let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap() 44 | // 為了保證清單順序,先弄幾個容器。 45 | var containerA: [TISInputSource.KeyboardLayout?] = [] 46 | var containerB: [TISInputSource.KeyboardLayout?] = [] 47 | var containerC: [TISInputSource.KeyboardLayout] = [] 48 | 49 | let filterSet = Array(Set(arrWhitelistedKeyLayoutsASCII).subtracting(Set(arrDynamicBasicKeyLayouts))) 50 | let matchedGroupBasic = (arrWhitelistedKeyLayoutsASCII + arrDynamicBasicKeyLayouts).compactMap { 51 | allTISKeyboardLayouts[$0] 52 | } 53 | for neta in matchedGroupBasic { 54 | if filterSet.contains(neta.id) { 55 | containerC.append(neta) 56 | } else if neta.id.hasPrefix("com.apple") { 57 | containerA.append(neta) 58 | } else { 59 | containerB.append(neta) 60 | } 61 | } 62 | 63 | // 這裡的 nil 是用來讓選單插入分隔符用的。 64 | if !containerA.isEmpty { containerA.append(nil) } 65 | if !containerB.isEmpty { containerB.append(nil) } 66 | 67 | return containerA + containerB + containerC 68 | } 69 | } 70 | 71 | // MARK: - 與輸入法的具體的安裝過程有關的命令 72 | 73 | extension IMKHelper { 74 | @discardableResult 75 | public static func registerInputMethod() -> Int32 { 76 | TISInputSource.registerInputMethod() ? 0 : -1 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Packages/vChewing_IMKUtils_IlimiImpl/Sources/IMKUtils/LatinKeyboardMappings.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The vChewing Project (MIT-NTL License). 2 | // ==================== 3 | // This code is released under the MIT license (SPDX-License-Identifier: MIT) 4 | // ... with NTL restriction stating that: 5 | // No trademark license is granted to use the trade names, trademarks, service 6 | // marks, or product names of Contributor, except as required to fulfill notice 7 | // requirements defined in MIT License. 8 | 9 | import Foundation 10 | 11 | public enum LatinKeyboardMappings: String, CaseIterable { 12 | case qwertyIlimi = "com.jefferson.inputmethod.ilimi.keylayout.IlimiKeyboard" 13 | case qwerty = "com.apple.keylayout.ABC" 14 | case qwertyBritish = "com.apple.keylayout.British" 15 | case qwertyUS = "com.apple.keylayout.US" // 10.9 - 10.12 16 | case azerty = "com.apple.keylayout.ABC-AZERTY" 17 | case qwertz = "com.apple.keylayout.ABC-QWERTZ" 18 | case azertyFrench = "com.apple.keylayout.French" // 10.9 - 10.12 19 | case qwertzGerman = "com.apple.keylayout.German" // 10.9 - 10.12 20 | case colemak = "com.apple.keylayout.Colemak" 21 | case dvorak = "com.apple.keylayout.Dvorak" 22 | case dvorakQwertyCMD = "com.apple.keylayout.DVORAK-QWERTYCMD" 23 | case dvorakLeft = "com.apple.keylayout.Dvorak-Left" 24 | case dvorakRight = "com.apple.keylayout.Dvorak-Right" 25 | 26 | // MARK: Public 27 | 28 | public var mapTable: [UInt16: (String, String)] { 29 | switch self { 30 | case .qwerty, .qwertyBritish, .qwertyIlimi, .qwertyUS: return Self.dictQwerty 31 | case .azerty, .azertyFrench: return Self.dictAzerty 32 | case .qwertz, .qwertzGerman: return Self.dictQwertz 33 | case .colemak: return Self.dictColemak 34 | case .dvorak, .dvorakQwertyCMD: return Self.dictDvorak 35 | case .dvorakLeft: return Self.dictDvorakLeft 36 | case .dvorakRight: return Self.dictDvorakRight 37 | } 38 | } 39 | 40 | // MARK: Private 41 | 42 | private static let dictQwerty: [UInt16: (String, String)] = [ 43 | 0: ("a", "A"), 1: ("s", "S"), 2: ("d", "D"), 3: ("f", "F"), 4: ("h", "H"), 5: ("g", "G"), 44 | 6: ("z", "Z"), 7: ("x", "X"), 8: ("c", "C"), 9: ("v", "V"), 11: ("b", "B"), 12: ("q", "Q"), 45 | 13: ("w", "W"), 14: ("e", "E"), 15: ("r", "R"), 16: ("y", "Y"), 17: ("t", "T"), 18: ("1", "!"), 46 | 19: ("2", "@"), 20: ("3", "#"), 21: ("4", "$"), 22: ("6", "^"), 23: ("5", "%"), 24: ("=", "+"), 47 | 25: ("9", "("), 26: ("7", "&"), 27: ("-", "_"), 28: ("8", "*"), 29: ("0", ")"), 30: ("]", "}"), 48 | 31: ("o", "O"), 32: ("u", "U"), 33: ("[", "{"), 34: ("i", "I"), 35: ("p", "P"), 37: ("l", "L"), 49 | 38: ("j", "J"), 39: ("\'", "\""), 40: ("k", "K"), 41: (";", ":"), 42: ("\\", "|"), 43: (",", "<"), 50 | 44: ("/", "?"), 45: ("n", "N"), 46: ("m", "M"), 47: (".", ">"), 50: ("`", "~"), 51 | ] 52 | 53 | private static let dictAzerty: [UInt16: (String, String)] = [ 54 | 0: ("q", "Q"), 1: ("s", "S"), 2: ("d", "D"), 3: ("f", "F"), 4: ("h", "H"), 5: ("g", "G"), 55 | 6: ("w", "W"), 7: ("x", "X"), 8: ("c", "C"), 9: ("v", "V"), 11: ("b", "B"), 12: ("a", "A"), 56 | 13: ("z", "Z"), 14: ("e", "E"), 15: ("r", "R"), 16: ("y", "Y"), 17: ("t", "T"), 18: ("&", "1"), 57 | 19: ("é", "2"), 20: ("\"", "3"), 21: ("\'", "4"), 22: ("§", "6"), 23: ("(", "5"), 24: ("-", "_"), 58 | 25: ("ç", "9"), 26: ("è", "7"), 27: (")", "°"), 28: ("!", "8"), 29: ("à", "0"), 30: ("$", "*"), 59 | 31: ("o", "O"), 32: ("u", "U"), 33: ("^", "¨"), 34: ("i", "I"), 35: ("p", "P"), 37: ("l", "L"), 60 | 38: ("j", "J"), 39: ("ù", "%"), 40: ("k", "K"), 41: ("m", "M"), 42: ("`", "£"), 43: (";", "."), 61 | 44: ("=", "+"), 45: ("n", "N"), 46: (",", "?"), 47: (":", "/"), 50: ("<", ">"), 62 | ] 63 | 64 | private static let dictQwertz: [UInt16: (String, String)] = [ 65 | 0: ("a", "A"), 1: ("s", "S"), 2: ("d", "D"), 3: ("f", "F"), 4: ("h", "H"), 5: ("g", "G"), 66 | 6: ("y", "Y"), 7: ("x", "X"), 8: ("c", "C"), 9: ("v", "V"), 11: ("b", "B"), 12: ("q", "Q"), 67 | 13: ("w", "W"), 14: ("e", "E"), 15: ("r", "R"), 16: ("z", "Z"), 17: ("t", "T"), 18: ("1", "!"), 68 | 19: ("2", "\""), 20: ("3", "§"), 21: ("4", "$"), 22: ("6", "&"), 23: ("5", "%"), 24: ("´", "`"), 69 | 25: ("9", ")"), 26: ("7", "/"), 27: ("ß", "?"), 28: ("8", "("), 29: ("0", "="), 30: ("+", "*"), 70 | 31: ("o", "O"), 32: ("u", "U"), 33: ("ü", "Ü"), 34: ("i", "I"), 35: ("p", "P"), 37: ("l", "L"), 71 | 38: ("j", "J"), 39: ("ä", "Ä"), 40: ("k", "K"), 41: ("ö", "Ö"), 42: ("#", "\'"), 43: (",", ";"), 72 | 44: ("-", "_"), 45: ("n", "N"), 46: ("m", "M"), 47: (".", ":"), 50: ("<", ">"), 73 | ] 74 | 75 | private static let dictColemak: [UInt16: (String, String)] = [ 76 | 0: ("a", "A"), 1: ("r", "R"), 2: ("s", "S"), 3: ("t", "T"), 4: ("h", "H"), 5: ("d", "D"), 77 | 6: ("z", "Z"), 7: ("x", "X"), 8: ("c", "C"), 9: ("v", "V"), 11: ("b", "B"), 12: ("q", "Q"), 78 | 13: ("w", "W"), 14: ("f", "F"), 15: ("p", "P"), 16: ("j", "J"), 17: ("g", "G"), 18: ("1", "!"), 79 | 19: ("2", "@"), 20: ("3", "#"), 21: ("4", "$"), 22: ("6", "^"), 23: ("5", "%"), 24: ("=", "+"), 80 | 25: ("9", "("), 26: ("7", "&"), 27: ("-", "_"), 28: ("8", "*"), 29: ("0", ")"), 30: ("]", "}"), 81 | 31: ("y", "Y"), 32: ("l", "L"), 33: ("[", "{"), 34: ("u", "U"), 35: (";", ":"), 37: ("i", "I"), 82 | 38: ("n", "N"), 39: ("\'", "\""), 40: ("e", "E"), 41: ("o", "O"), 42: ("\\", "|"), 43: (",", "<"), 83 | 44: ("/", "?"), 45: ("k", "K"), 46: ("m", "M"), 47: (".", ">"), 50: ("`", "~"), 84 | ] 85 | 86 | private static let dictDvorak: [UInt16: (String, String)] = [ 87 | 0: ("a", "A"), 1: ("o", "O"), 2: ("e", "E"), 3: ("u", "U"), 4: ("d", "D"), 5: ("i", "I"), 88 | 6: (";", ":"), 7: ("q", "Q"), 8: ("j", "J"), 9: ("k", "K"), 11: ("x", "X"), 12: ("\'", "\""), 89 | 13: (",", "<"), 14: (".", ">"), 15: ("p", "P"), 16: ("f", "F"), 17: ("y", "Y"), 18: ("1", "!"), 90 | 19: ("2", "@"), 20: ("3", "#"), 21: ("4", "$"), 22: ("6", "^"), 23: ("5", "%"), 24: ("]", "}"), 91 | 25: ("9", "("), 26: ("7", "&"), 27: ("[", "{"), 28: ("8", "*"), 29: ("0", ")"), 30: ("=", "+"), 92 | 31: ("r", "R"), 32: ("g", "G"), 33: ("/", "?"), 34: ("c", "C"), 35: ("l", "L"), 37: ("n", "N"), 93 | 38: ("h", "H"), 39: ("-", "_"), 40: ("t", "T"), 41: ("s", "S"), 42: ("\\", "|"), 43: ("w", "W"), 94 | 44: ("z", "Z"), 45: ("b", "B"), 46: ("m", "M"), 47: ("v", "V"), 50: ("`", "~"), 95 | ] 96 | 97 | private static let dictDvorakLeft: [UInt16: (String, String)] = [ 98 | 0: ("-", "_"), 1: ("k", "K"), 2: ("c", "C"), 3: ("d", "D"), 4: ("h", "H"), 5: ("t", "T"), 99 | 6: ("\'", "\""), 7: ("x", "X"), 8: ("g", "G"), 9: ("v", "V"), 11: ("w", "W"), 12: (";", ":"), 100 | 13: ("q", "Q"), 14: ("b", "B"), 15: ("y", "Y"), 16: ("r", "R"), 17: ("u", "U"), 18: ("[", "{"), 101 | 19: ("]", "}"), 20: ("/", "?"), 21: ("p", "P"), 22: ("m", "M"), 23: ("f", "F"), 24: ("1", "!"), 102 | 25: ("4", "$"), 26: ("l", "L"), 27: ("2", "@"), 28: ("j", "J"), 29: ("3", "#"), 30: ("=", "+"), 103 | 31: (".", ">"), 32: ("s", "S"), 33: ("5", "%"), 34: ("o", "O"), 35: ("6", "^"), 37: ("z", "Z"), 104 | 38: ("e", "E"), 39: ("7", "&"), 40: ("a", "A"), 41: ("8", "*"), 42: ("\\", "|"), 43: (",", "<"), 105 | 44: ("9", "("), 45: ("n", "N"), 46: ("i", "I"), 47: ("0", ")"), 50: ("`", "~"), 106 | ] 107 | 108 | private static let dictDvorakRight: [UInt16: (String, String)] = [ 109 | 0: ("7", "&"), 1: ("8", "*"), 2: ("z", "Z"), 3: ("a", "A"), 4: ("h", "H"), 5: ("e", "E"), 110 | 6: ("9", "("), 7: ("0", ")"), 8: ("x", "X"), 9: (",", "<"), 11: ("i", "I"), 12: ("5", "%"), 111 | 13: ("6", "^"), 14: ("q", "Q"), 15: (".", ">"), 16: ("r", "R"), 17: ("o", "O"), 18: ("1", "!"), 112 | 19: ("2", "@"), 20: ("3", "#"), 21: ("4", "$"), 22: ("l", "L"), 23: ("j", "J"), 24: ("]", "}"), 113 | 25: ("p", "P"), 26: ("m", "M"), 27: ("[", "{"), 28: ("f", "F"), 29: ("/", "?"), 30: ("=", "+"), 114 | 31: ("y", "Y"), 32: ("s", "S"), 33: (";", ":"), 34: ("u", "U"), 35: ("b", "B"), 37: ("c", "C"), 115 | 38: ("t", "T"), 39: ("-", "_"), 40: ("d", "D"), 41: ("k", "K"), 42: ("\\", "|"), 43: ("v", "V"), 116 | 44: ("\'", "\""), 45: ("n", "N"), 46: ("w", "W"), 47: ("g", "G"), 50: ("`", "~"), 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /Packages/vChewing_IMKUtils_IlimiImpl/Tests/IMKUtilsTests/IMKUtilsTests.swift: -------------------------------------------------------------------------------- 1 | @testable import IMKUtils 2 | import InputMethodKit 3 | import XCTest 4 | 5 | final class IMKUtilsTests: XCTestCase { 6 | func testPrintAllTISLayoutIdentifiers() throws { 7 | for item in TISInputSource.getAllTISInputKeyboardLayoutMap() { 8 | print(item.key) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ilimi 一粒米輸入法 2 | 要什麼工具就DIY,於是我……利用[InputMethodKit](https://developer.apple.com/documentation/inputmethodkit)開發的仿蝦米 3 | 4 | 最低系統要求: macOS 13.0+ Ventura. 5 | 6 | --- 7 | 8 | ## 字檔說明 9 | 10 | ⚠️ 因尚未釐清嘸蝦米版權問題,如同[肥米輸入法](https://github.com/shadowjohn/UCL_LIU),一粒米輸入法暫不直接發布字根檔。 11 | 12 | 目前一粒米支援: 13 | 1. 各版liu.cin 14 | 2. 肥米輸入法的liu.json 15 | 3. liu-uni.tab 16 | 17 | 歡迎發信至 *y1lichen@icloud.com* 或是依照肥米輸入法的說明生成liu.json檔案。 18 | 19 | ## 安裝說明 20 | 21 | 建置方式有二: 22 | 1. 感謝**威注音作者ShikiSuen**的協作,一粒米已有[安裝檔](https://github.com/y1lichen/ilimi-inputmethod/releases)可供下載。 23 | 下載並執行完installer後請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。 24 | 2. 目前repo內提供build.sh,使用方式如下: 25 | ``` 26 | git clone https://github.com/y1lichen/ilimi-inputmethod.git 27 | cd ilimi-inputmethod 28 | chmod +x ./build.sh 29 | ./build.sh 30 | ``` 31 | 32 | **下載並啟用一粒米後,一定要點選menubar上一粒米選單的開啟使用者設定目錄,並將字檔放自動開啟之資料夾中,再點選匯入字檔** 33 | 34 | ## 功能(未完成、仍持續新增) 35 | 36 | 1. 一般輸入 37 | 38 | ![一般輸入](https://github.com/y1lichen/ilimi-inputmethod/blob/main/media/demo01.gif) 39 | 40 | 2. 打繁出簡 41 | 輸入,,CT切換打繁出簡模 42 | 43 | ![打繁出簡](https://github.com/y1lichen/ilimi-inputmethod/blob/main/media/demo02.gif) 44 | 45 | 3. 加v、r、s等輔助選字 46 | 4. 注音輸入 47 | 48 | ![注音輸入](https://github.com/y1lichen/ilimi-inputmethod/blob/main/media/zhuyin_demo.gif) 49 | 50 | 輸入';即可使用注音輸入 51 | 52 | 5. SP快打模式 53 | 54 | 輸入,,sp可進入快打模式。若輸入字碼不是最簡碼會顯示該字最簡碼,並要求使用者重輸。 55 | 56 | 最簡碼機制如下: 57 | - 使用liu-uni.tab字根檔:直接使用字根檔中之標注 58 | - 使用liu.cin、liu.json等字檔:尋找輸入文字之最簡字碼。若最簡字碼不只一者,則利用輸入這些最簡字碼時該文字的順位決定是否為最簡碼。 59 | 60 | 6. 英數模式 61 | 62 | 使用CapsLock即可切換英數模式 63 | 64 | ![英數模式](https://github.com/y1lichen/ilimi-inputmethod/blob/main/media/ascii_demo.gif) 65 | 66 | 7. 反查注音、輸入碼 67 | 68 | 輸入 **,,q** 可快速開啟反查注音/查碼畫面 69 | 70 | ![反查](https://github.com/y1lichen/ilimi-inputmethod/blob/main/media/demo03.gif) 71 | 72 | 8. 同音輸入 73 | 74 | 輸入\後再輸入文字,就會出現和輸入的字同音的字 75 | 76 | ![同音輸入](https://github.com/y1lichen/ilimi-inputmethod/blob/main/media/demo04.gif) 77 | 78 | 9. 全形模式 79 | 80 | 輸入shift+空白鍵可以進入全形模式 81 | 82 | 10. 自訂加詞 83 | ![自訂加詞](https://github.com/y1lichen/ilimi-inputmethod/blob/main/media/custom_phrase_demo.png) 84 | 85 | ## 可自定義項目 86 | 87 | - 選字窗字體大小 88 | - 選字窗樣式(直式、橫式) 89 | - 是否只顯示完全匹配輸入字碼之字元 90 | - 是否在沒有候選字時限制輸入(在沒候選字時按下enter可以直接輸入英文字母) 91 | - 是否在使用注音輸入後提示拆碼 92 | - 靜音模式(在輸入錯誤時發出beep) 93 | - 使用數字選字時的選字碼為0-8或1-9 94 | - 是否自動檢查更新 95 | 96 | ## 備註 97 | 98 | - 因為直式選字窗scrollable的特性,候選字號碼並不完全依候選字窗頁數改變,因此只有在首頁時可以使用數字鍵選字。這對嘸蝦米輸入機制來說不會有太大影響。 99 | 100 | ## Reference 101 | 102 | 本專案的IMK機制參考 2.x 版本的[vChewing威注音](https://vchewing.github.io/README.html),該專案的源碼對IMK許多函式有清楚註解 103 | 104 | [https://mzp.hatenablog.com/entry/2017/09/17/220320](https://mzp.hatenablog.com/entry/2017/09/17/220320) 105 | [https://arika.org/2022/04/02/macos-inputmethodkit/](https://arika.org/2022/04/02/macos-inputmethodkit/) 106 | 107 | --- 108 | 109 | 打繁出簡模式的「繁體字轉簡體字」程式碼是由[GBig](https://github.com/RockfordWei/GBig)修改而來,利用dictionary加速查找速度。 110 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | killall -9 ilimi 2 | 3 | rm -rf ~/Library/Input\ Methods/ilimi.app 4 | rm -rf ~/Library/Input\ Methods/ilimi.swiftmodule 5 | rm -rf ~/Library/Containers/com.lennylxx.inputmethod.ilimi/ 6 | rm -rf ~/Library/Developer/Xcode/DerivedData/ilimi-*/ 7 | rm -rf ./build 8 | 9 | xcodebuild -scheme ilimi build CONFIGURATION_BUILD_DIR=/Users/$(id -un)/Library/Input\ Methods/ 10 | 11 | ls -al ~/Library/Input\ Methods 12 | -------------------------------------------------------------------------------- /ilimi.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ilimi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ilimi.xcodeproj/project.xcworkspace/xcuserdata/chenli.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | CustomLocation 7 | CustomBuildIntermediatesPath 8 | Build/Intermediates.noindex 9 | CustomBuildLocationType 10 | RelativeToWorkspace 11 | CustomBuildProductsPath 12 | Build/Products 13 | DerivedDataLocationStyle 14 | Default 15 | ShowSharedSchemesAutomaticallyEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /ilimi.xcodeproj/xcshareddata/xcschemes/ilimi.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ilimi.xcodeproj/xcuserdata/chenli.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /ilimi.xcodeproj/xcuserdata/chenli.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ilimi.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | ilimiInstaller.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | D4FC14C928C649F70081BCC0 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /ilimi/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ilimi/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | LIMI 21 | CFBundleSupportedPlatforms 22 | 23 | MacOSX 24 | 25 | CFBundleVersion 26 | $(CURRENT_PROJECT_VERSION) 27 | CFEULAContent 28 | License texts used in the customized about window. 29 | InputMethodConnectionName 30 | $(PRODUCT_BUNDLE_IDENTIFIER)_Connection 31 | InputMethodServerControllerClass 32 | IlimiInputController 33 | InputMethodServerDataSourceClass 34 | IlimiInputController 35 | InputMethodServerDelegateClass 36 | IlimiInputController 37 | InputMethodServerPreferencesWindowControllerClass 38 | Preferences 39 | InputMethodSessionController 40 | IlimiInputController 41 | LSApplicationCategoryType 42 | public.app-category.utilities 43 | LSHasLocalizedDisplayName 44 | 45 | LSMinimumSystemVersion 46 | ${MACOSX_DEPLOYMENT_TARGET} 47 | LSUIElement 48 | 49 | NSHumanReadableCopyright 50 | (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 51 | NSPrincipalClass 52 | NSApplication 53 | NSRequiresAquaSystemAppearance 54 | No 55 | NSSupportsSuddenTermination 56 | 57 | TICapsLockLanguageSwitchCapable 58 | 59 | TISIconIsTemplate 60 | 61 | TISIconLabels 62 | 63 | Primary 64 | 粒米 65 | 66 | TISInputSourceID 67 | com.jefferson.inputmethod.ilimi 68 | TISIntendedLanguage 69 | zh-Hant 70 | TISParticipatesInTouchBar 71 | 72 | tsInputMethodCharacterRepertoireKey 73 | 74 | Hant 75 | Hans 76 | Hani 77 | Hanb 78 | Han 79 | 80 | tsInputMethodIconFileKey 81 | MenuIcon-ILIMI.png 82 | tsInputModeIsVisibleKey 83 | 84 | tsInputModeMenuIconFileKey 85 | MenuIcon-ILIMI.png 86 | tsInputModePaletteIconFileKey 87 | MenuIcon-ILIMI.png 88 | tsInputModePrimaryInScriptKey 89 | 90 | tsInputModeScriptKey 91 | smUnicode 92 | tsVisibleInputModeOrderedArrayKey 93 | 94 | com.jefferson.inputmethod.ilimi 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ilimi/MenuIcons/MenuIcon-ILIMI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/MenuIcons/MenuIcon-ILIMI.png -------------------------------------------------------------------------------- /ilimi/MenuIcons/MenuIcon-ILIMI@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimi/MenuIcons/MenuIcon-ILIMI@2x.png -------------------------------------------------------------------------------- /ilimi/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ilimi/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | CFBundleName = "ilimi-IME"; 4 | CFBundleDisplayName = "ilimi-IME"; 5 | NSHumanReadableCopyright = "© 2022 and onwards The ilimi-IME Project (3-Clause BSD license)."; 6 | "com.jefferson.inputmethod.ilimi" = "ilimi-IME"; 7 | -------------------------------------------------------------------------------- /ilimi/ilimi.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.temporary-exception.mach-register.global-name 6 | $(PRODUCT_BUNDLE_IDENTIFIER)_Connection 7 | 8 | 9 | -------------------------------------------------------------------------------- /ilimi/main.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | import Foundation 7 | import ilimiMainAssembly 8 | import IMKUtils 9 | import InputMethodKit 10 | 11 | let cmdParameters = CommandLine.arguments.dropFirst(1) 12 | 13 | switch cmdParameters.count { 14 | case 0: break 15 | 16 | case 1: 17 | switch cmdParameters.first?.lowercased() { 18 | case "install": 19 | let exitCode = IMKHelper.registerInputMethod() 20 | exit(exitCode) 21 | default: break 22 | } 23 | exit(0) 24 | default: exit(0) 25 | } 26 | 27 | guard let server = IMKServer( 28 | name: Bundle.main.infoDictionary?["InputMethodConnectionName"] as? String, 29 | bundleIdentifier: Bundle.main.bundleIdentifier 30 | ) else { 31 | NSLog( 32 | "ilimiDebug: Fatal error: Cannot initialize input method server with connection name retrieved from the plist, nor there's no connection name in the plist." 33 | ) 34 | exit(-1) 35 | } 36 | 37 | public let theServer = server 38 | 39 | NSApplication.shared.delegate = AppDelegate.shared 40 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 41 | -------------------------------------------------------------------------------- /ilimi/zh-Hant.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | CFBundleName = "一粒米輸入法"; 4 | CFBundleDisplayName = "一粒米輸入法"; 5 | NSHumanReadableCopyright = "© 2022 and onwards The ilimi-IME Project (3-Clause BSD license)."; 6 | "com.jefferson.inputmethod.ilimi" = "一粒米輸入法"; 7 | -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/IconSansMargin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IconSansMargin.heic", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ilimiInstaller/Assets.xcassets/IconSansMargin.imageset/IconSansMargin.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/ilimiInstaller/Assets.xcassets/IconSansMargin.imageset/IconSansMargin.heic -------------------------------------------------------------------------------- /ilimiInstaller/Installer-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | MBIN 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | CFEULAContent 24 | (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license).\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS IS&quot;\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSHasLocalizedDisplayName 28 | 29 | LSMinimumSystemVersion 30 | ${MACOSX_DEPLOYMENT_TARGET} 31 | NSHumanReadableCopyright 32 | (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 33 | NSMainNibFile 34 | MainMenu 35 | NSPrincipalClass 36 | NSApplication 37 | 38 | 39 | -------------------------------------------------------------------------------- /ilimiInstaller/InstallerShared.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | import InputMethodKit 7 | import SwiftUI 8 | 9 | public let kTargetBin = "ilimi" 10 | public let kTargetBinPhraseEditor = "ilimiPhraseEditor" 11 | public let kTargetType = "app" 12 | public let kTargetBundle = "ilimi.app" 13 | public let kTargetBundleWithComponents = "Library/Input%20Methods/ilimi.app" 14 | public let kTISInputSourceID = "com.jefferson.inputmethod.ilimi" 15 | 16 | let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/ilimi.app") 17 | 18 | public let realHomeDir = URL( 19 | fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil 20 | ) 21 | public let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods") 22 | public let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents) 23 | public let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS") 24 | .appendingPathComponent(kTargetBin) 25 | 26 | public let kDestinationPartial = urlDestinationPartial.path 27 | public let kTargetPartialPath = urlTargetPartial.path 28 | public let kTargetFullBinPartialPath = urlTargetFullBinPartial.path 29 | 30 | public let kTranslocationRemovalTickInterval: TimeInterval = 0.5 31 | public let kTranslocationRemovalDeadline: TimeInterval = 60.0 32 | 33 | public let installingVersion = Bundle.main 34 | .infoDictionary?[kCFBundleVersionKey as String] as? String ?? "BAD_INSTALLING_VER" 35 | public let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "BAD_VER_STR" 36 | public let copyrightLabel = Bundle.main 37 | .localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL" 38 | public let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String ?? "BAD_EULA_CONTENT" 39 | 40 | public var mainWindowTitle: String { 41 | "i18n:installer.INSTALLER_APP_TITLE_FULL".i18n + " (v\(versionString), Build \(installingVersion))" 42 | } 43 | 44 | var allRegisteredInstancesOfThisInputMethod: [TISInputSource] { 45 | guard let components = Bundle(url: imeURLInstalled)?.infoDictionary?["ComponentInputModeDict"] as? [String: Any], 46 | let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any] 47 | else { 48 | return [] 49 | } 50 | return TISInputSource.match(modeIDs: tsInputModeListKey.keys.map(\.description)) 51 | } 52 | 53 | // MARK: - NSApp Activation Helper 54 | 55 | // This is to deal with changes brought by macOS 14. 56 | 57 | extension NSApplication { 58 | public func popup() { 59 | #if compiler(>=5.9) && canImport(AppKit, _version: "14.0") 60 | if #available(macOS 14.0, *) { 61 | NSApp.activate() 62 | } else { 63 | NSApp.activate(ignoringOtherApps: true) 64 | } 65 | #else 66 | NSApp.activate(ignoringOtherApps: true) 67 | #endif 68 | } 69 | } 70 | 71 | // MARK: - KeyWindow Finder 72 | 73 | extension NSApplication { 74 | public var keyWindows: [NSWindow] { 75 | NSApp.windows.filter(\.isKeyWindow) 76 | } 77 | } 78 | 79 | // MARK: - NSApp End With Delay 80 | 81 | extension NSApplication { 82 | public func terminateWithDelay() { 83 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { [weak self] in 84 | if let this = self { 85 | this.terminate(this) 86 | } 87 | } 88 | } 89 | } 90 | 91 | // MARK: - AlertIntel 92 | 93 | public struct AlertIntel {} 94 | 95 | // MARK: - AlertType 96 | 97 | public enum AlertType: String, Identifiable { 98 | case nothing, installationFailed, missingAfterRegistration, postInstallAttention, postInstallWarning, postInstallOK 99 | 100 | // MARK: Public 101 | 102 | public var id: String { rawValue } 103 | 104 | // MARK: Internal 105 | 106 | var title: LocalizedStringKey { 107 | switch self { 108 | case .nothing: return "" 109 | case .installationFailed: return "Install Failed" 110 | case .missingAfterRegistration: return "Fatal Error" 111 | case .postInstallAttention: return "Attention" 112 | case .postInstallWarning: return "Warning" 113 | case .postInstallOK: return "Installation Successful" 114 | } 115 | } 116 | 117 | var message: String { 118 | switch self { 119 | case .nothing: return "" 120 | case .installationFailed: 121 | return "Cannot copy the file to the destination.".i18n 122 | 123 | case .missingAfterRegistration: 124 | return String( 125 | format: "Cannot find input source %@ after registration.".i18n, 126 | kTISInputSourceID 127 | ) 128 | 129 | case .postInstallAttention: 130 | return "ilimi is upgraded, but please log out or reboot for the new version to be fully functional.".i18n 131 | 132 | case .postInstallWarning: 133 | return "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." 134 | .i18n 135 | 136 | case .postInstallOK: 137 | return "ilimi is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." 138 | .i18n 139 | } 140 | } 141 | } 142 | 143 | extension StringLiteralType { 144 | fileprivate var i18n: String { NSLocalizedString(description, comment: "") } 145 | } 146 | 147 | // MARK: - Shell 148 | 149 | extension NSApplication { 150 | public func shell(_ command: String) throws -> String { 151 | let task = Process() 152 | let pipe = Pipe() 153 | 154 | task.standardOutput = pipe 155 | task.standardError = pipe 156 | task.arguments = ["-c", command] 157 | if #available(macOS 10.13, *) { 158 | task.executableURL = URL(fileURLWithPath: "/bin/zsh") 159 | } else { 160 | task.launchPath = "/bin/zsh" 161 | } 162 | task.standardInput = nil 163 | 164 | if #available(macOS 10.13, *) { 165 | try task.run() 166 | } else { 167 | task.launch() 168 | } 169 | 170 | var output = "" 171 | do { 172 | let data = try pipe.fileHandleForReading.readToEnd() 173 | if let data = data, let str = String(data: data, encoding: .utf8) { 174 | output.append(str) 175 | } 176 | } catch { 177 | return "" 178 | } 179 | return output 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /ilimiInstaller/MainView.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | import SwiftUI 7 | 8 | public struct MainView: View { 9 | // MARK: Lifecycle 10 | 11 | public init() { 12 | if FileManager.default.fileExists(atPath: kTargetPartialPath) { 13 | let currentBundle = Bundle(path: kTargetPartialPath) 14 | let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String 15 | let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String 16 | if shortVersion != nil, let currentVersion = currentVersion, 17 | currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending { 18 | self.isUpgrading = true 19 | } 20 | } 21 | } 22 | 23 | // MARK: Public 24 | 25 | public var body: some View { 26 | GroupBox { 27 | VStack(alignment: .leading, spacing: 6) { 28 | VStack(alignment: .leading) { 29 | HStack(alignment: .center) { 30 | if let icon = NSImage(named: "IconSansMargin") { 31 | Image(nsImage: icon).resizable().frame(width: 90, height: 90) 32 | } 33 | VStack(alignment: .leading) { 34 | HStack { 35 | Text("i18n:installer.APP_NAME").fontWeight(.heavy).lineLimit(1) 36 | Text("v\(versionString) Build \(installingVersion)").lineLimit(1) 37 | }.fixedSize() 38 | Text(Self.strCopyrightLabel).font(.custom("Tahoma", size: 11)) 39 | Text("i18n:installer.DEV_CREW").font(.custom("Tahoma", size: 11)).padding([.vertical], 2) 40 | } 41 | } 42 | GroupBox(label: Text("i18n:installer.LICENSE_TITLE")) { 43 | ScrollView(.vertical, showsIndicators: true) { 44 | HStack { 45 | Text(eulaContent).textSelection(.enabled) 46 | .frame(maxWidth: 455) 47 | .font(.custom("Tahoma", size: 11)) 48 | Spacer() 49 | } 50 | }.padding(4).frame(height: 128) 51 | } 52 | Text("i18n:installer.EULA_PROMPT_NOTICE").bold().padding(.bottom, 2) 53 | } 54 | Divider() 55 | HStack(alignment: .top) { 56 | Text("i18n:installer.REPO_URL_TEXT") 57 | .font(.custom("Tahoma", size: 11)) 58 | .opacity(0.5) 59 | .frame(maxWidth: .infinity) 60 | VStack(spacing: 4) { 61 | Button { installationButtonClicked() } label: { 62 | Text(isUpgrading ? "i18n:installer.DO_APP_UPGRADE" : "i18n:installer.ACCEPT_INSTALLATION") 63 | .bold().frame(width: 114) 64 | } 65 | .keyboardShortcut(.defaultAction) 66 | .disabled(!isCancelButtonEnabled) 67 | Button(role: .cancel) { NSApp.terminateWithDelay() } label: { 68 | Text("i18n:installer.CANCEL_INSTALLATION").frame(width: 114) 69 | } 70 | .keyboardShortcut(.cancelAction) 71 | .disabled(!isAgreeButtonEnabled) 72 | }.fixedSize(horizontal: true, vertical: true) 73 | } 74 | Spacer() 75 | } 76 | .font(.custom("Tahoma", size: 12)) 77 | .padding(4) 78 | } 79 | // ALERTS 80 | .alert(AlertType.installationFailed.title, isPresented: $isShowingAlertForFailedInstallation) { 81 | Button(role: .cancel) { NSApp.terminateWithDelay() } label: { Text("Cancel") } 82 | } message: { 83 | Text(AlertType.installationFailed.message) 84 | } 85 | .alert(AlertType.missingAfterRegistration.title, isPresented: $isShowingAlertForMissingPostInstall) { 86 | Button(role: .cancel) { NSApp.terminateWithDelay() } label: { Text("Abort") } 87 | } message: { 88 | Text(AlertType.missingAfterRegistration.message) 89 | } 90 | .alert(currentAlertContent.title, isPresented: $isShowingPostInstallNotification) { 91 | Button(role: .cancel) { NSApp.terminateWithDelay() } label: { 92 | Text(currentAlertContent == .postInstallWarning ? "Continue" : "OK") 93 | } 94 | } message: { 95 | Text(currentAlertContent.message) 96 | } 97 | // SHEET FOR STOPPING THE OLD VERSION 98 | .sheet(isPresented: $pendingSheetPresenting) { 99 | // TODO: Tasks after sheet gets closed by `dismiss()`. 100 | } content: { 101 | Text("i18n:installer.STOPPING_THE_OLD_VERSION").frame(width: 407, height: 144) 102 | .onReceive(timer) { _ in 103 | if timeRemaining > 0 { 104 | if Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) == false { 105 | pendingSheetPresenting = false 106 | isTranslocationFinished = true 107 | installInputMethod( 108 | previousExists: true, 109 | previousVersionNotFullyDeactivatedWarning: false 110 | ) 111 | } 112 | timeRemaining -= 1 113 | } else { 114 | pendingSheetPresenting = false 115 | isTranslocationFinished = false 116 | installInputMethod( 117 | previousExists: true, 118 | previousVersionNotFullyDeactivatedWarning: true 119 | ) 120 | } 121 | } 122 | } 123 | // OTHER 124 | .padding(12) 125 | .frame(width: 533, alignment: .topLeading) 126 | .navigationTitle(mainWindowTitle) 127 | .fixedSize() 128 | .foregroundStyle(Color(nsColor: NSColor.textColor)) 129 | .background(Color(nsColor: NSColor.windowBackgroundColor)) 130 | .clipShape(RoundedRectangle(cornerRadius: 16)) 131 | .frame( 132 | minWidth: 533, 133 | idealWidth: 533, 134 | maxWidth: 533, 135 | minHeight: 386, 136 | idealHeight: 386, 137 | maxHeight: 386, 138 | alignment: .top 139 | ) 140 | } 141 | 142 | // MARK: Internal 143 | 144 | static let strCopyrightLabel = Bundle.main 145 | .localizedInfoDictionary?["NSHumanReadableCopyright"] as? String ?? "BAD_COPYRIGHT_LABEL" 146 | 147 | @State var pendingSheetPresenting = false 148 | @State var isShowingAlertForFailedInstallation = false 149 | @State var isShowingAlertForMissingPostInstall = false 150 | @State var isShowingPostInstallNotification = false 151 | @State var currentAlertContent: AlertType = .nothing 152 | @State var isCancelButtonEnabled = true 153 | @State var isAgreeButtonEnabled = true 154 | @State var isPreviousVersionNotFullyDeactivated = false 155 | @State var isTranslocationFinished: Bool? 156 | @State var isUpgrading = false 157 | 158 | var translocationRemovalStartTime: Date? 159 | 160 | @State var timeRemaining = 60 161 | let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 162 | 163 | func installationButtonClicked() { 164 | isCancelButtonEnabled = false 165 | isAgreeButtonEnabled = false 166 | removeThenInstallInputMethod() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /ilimiInstaller/MainViewImpl.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | import IMKUtils 7 | import InputMethodKit 8 | 9 | extension MainView { 10 | public func removeThenInstallInputMethod() { 11 | let shouldWaitForTranslocationRemoval = Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) 12 | 13 | // 將既存輸入法扔到垃圾桶內 14 | do { 15 | let sourceDir = kDestinationPartial 16 | let fileManager = FileManager.default 17 | let fileURLString = sourceDir + "/" + kTargetBundle 18 | let fileURL = URL(fileURLWithPath: fileURLString) 19 | 20 | // 檢查檔案是否存在 21 | if fileManager.fileExists(atPath: fileURLString) { 22 | // 塞入垃圾桶 23 | try fileManager.trashItem(at: fileURL, resultingItemURL: nil) 24 | } else { 25 | NSLog("File does not exist") 26 | } 27 | } catch let error as NSError { 28 | NSLog("An error took place: \(error)") 29 | } 30 | 31 | let killTask = Process() 32 | killTask.launchPath = "/usr/bin/killall" 33 | killTask.arguments = [kTargetBin] 34 | killTask.launch() 35 | killTask.waitUntilExit() 36 | 37 | let killTask2 = Process() 38 | killTask2.launchPath = "/usr/bin/killall" 39 | killTask2.arguments = [kTargetBinPhraseEditor] 40 | killTask2.launch() 41 | killTask2.waitUntilExit() 42 | 43 | if shouldWaitForTranslocationRemoval { 44 | pendingSheetPresenting = true 45 | } else { 46 | installInputMethod( 47 | previousExists: false, previousVersionNotFullyDeactivatedWarning: false 48 | ) 49 | } 50 | } 51 | 52 | public func installInputMethod( 53 | previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool 54 | ) { 55 | guard let targetBundle = Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) 56 | else { 57 | return 58 | } 59 | let cpTask = Process() 60 | cpTask.launchPath = "/bin/cp" 61 | print(kDestinationPartial) 62 | cpTask.arguments = [ 63 | "-R", targetBundle, kDestinationPartial, 64 | ] 65 | cpTask.launch() 66 | cpTask.waitUntilExit() 67 | 68 | if cpTask.terminationStatus != 0 { 69 | isShowingAlertForFailedInstallation = true 70 | NSApp.terminateWithDelay() 71 | } 72 | 73 | _ = try? NSApp.shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)") 74 | 75 | guard let theBundle = Bundle(url: imeURLInstalled), 76 | let imeIdentifier = theBundle.bundleIdentifier 77 | else { 78 | NSApp.terminateWithDelay() 79 | return 80 | } 81 | 82 | let imeBundleURL = theBundle.bundleURL 83 | 84 | if allRegisteredInstancesOfThisInputMethod.isEmpty { 85 | NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).") 86 | let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr) 87 | if !status { 88 | isShowingAlertForMissingPostInstall = true 89 | NSApp.terminateWithDelay() 90 | } 91 | 92 | if allRegisteredInstancesOfThisInputMethod.isEmpty { 93 | let message = String( 94 | format: NSLocalizedString( 95 | "Cannot find input source %@ after registration.", comment: "" 96 | ) + "(#D41J0U8U)", 97 | imeIdentifier 98 | ) 99 | NSLog(message) 100 | } 101 | } 102 | 103 | var mainInputSourceEnabled = false 104 | 105 | allRegisteredInstancesOfThisInputMethod.forEach { neta in 106 | let isActivated = neta.isActivated 107 | defer { 108 | // 如果使用者在升級安裝或再次安裝之前已經有啟用威注音任一簡繁模式的話,則標記安裝成功。 109 | // 這樣可以尊重某些使用者「僅使用簡體中文」或「僅使用繁體中文」的習慣。 110 | mainInputSourceEnabled = mainInputSourceEnabled || isActivated 111 | } 112 | if isActivated { return } 113 | // WARNING: macOS 12 may return false positives, hence forced activation. 114 | if neta.activate() { 115 | NSLog("Input method enabled: \(imeIdentifier)") 116 | } else { 117 | NSLog("Failed to enable input method: \(imeIdentifier)") 118 | } 119 | } 120 | 121 | // Alert Panel 122 | if warning { 123 | currentAlertContent = .postInstallAttention 124 | } else if !mainInputSourceEnabled { 125 | currentAlertContent = .postInstallWarning 126 | } else { 127 | currentAlertContent = .postInstallOK 128 | } 129 | isShowingPostInstallNotification = true 130 | NSApp.terminateWithDelay() 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /ilimiInstaller/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ilimiInstaller/RelocationDetector.swift: -------------------------------------------------------------------------------- 1 | // (c) 2011 and onwards Lukhnos Liu and MJHsieh. 2 | // Swiftified by Rob Mayoff. 3 | // ==================== 4 | // This code is released under the MIT license (SPDX-License-Identifier: MIT) 5 | 6 | import Foundation 7 | 8 | public enum Reloc { 9 | // Determines if an app is translocated by Gatekeeper to a randomized path. 10 | // See https://weblog.rogueamoeba.com/2016/06/29/sierra-and-gatekeeper-path-randomization/ 11 | // Originally written by MJHsieh and Lukhnos Liu in Objective-C (MIT License). 12 | // Swiftified by: Rob Mayoff. Ref: https://forums.swift.org/t/58719/5 13 | public static func isAppBundleTranslocated(atPath bundlePath: String) -> Bool { 14 | var entryCount = getfsstat(nil, 0, 0) 15 | var entries: [statfs] = .init(repeating: .init(), count: Int(entryCount)) 16 | let absPath = bundlePath.cString(using: .utf8) 17 | entryCount = getfsstat(&entries, entryCount * Int32(MemoryLayout.stride), MNT_NOWAIT) 18 | for entry in entries.prefix(Int(entryCount)) { 19 | let isMatch = withUnsafeBytes(of: entry.f_mntfromname) { mntFromName in 20 | strcmp(absPath, mntFromName.baseAddress) == 0 21 | } 22 | if isMatch { 23 | var stat = statfs() 24 | let rc = statfs(absPath, &stat) 25 | return rc == 0 26 | } 27 | } 28 | return false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ilimiInstaller/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | CFBundleName = "ilimi-IME Installer"; 4 | NSHumanReadableCopyright = "© 2022 and onwards The ilimi-IME Project (3-Clause BSD license)."; 5 | CFEULAContent = "(c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license).\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"; 6 | -------------------------------------------------------------------------------- /ilimiInstaller/en.lproj/Installer-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | MBIN 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | CFEULAContent 24 | (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license).\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS IS&quot;\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSHasLocalizedDisplayName 28 | 29 | LSMinimumSystemVersion 30 | ${MACOSX_DEPLOYMENT_TARGET} 31 | NSHumanReadableCopyright 32 | (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 33 | NSMainNibFile 34 | MainMenu 35 | NSPrincipalClass 36 | NSApplication 37 | 38 | 39 | -------------------------------------------------------------------------------- /ilimiInstaller/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "Abort" = "Abort"; 2 | "Attention" = "Attention"; 3 | "Cancel" = "Cancel"; 4 | "Cannot activate the input method." = "Cannot activate the input method."; 5 | "Cannot copy the file to the destination." = "Cannot copy the file to the destination."; 6 | "Cannot find input source %@ after registration." = "Cannot find input source %@ after registration."; 7 | "Cannot register input source %@ at %@." = "Cannot register input source %@ at %@."; 8 | "Continue" = "Continue"; 9 | "Fatal Error" = "Fatal Error"; 10 | "i18n:installer.ACCEPT_INSTALLATION" = "I Accept"; 11 | "i18n:installer.APP_NAME" = "ilimi for macOS"; 12 | "i18n:installer.CANCEL_INSTALLATION" = "Cancel"; 13 | "i18n:installer.DEV_CREW" = "ilimi macOS Development: Jefferson Chen, reinforced by Shiki Suen."; 14 | "i18n:installer.DO_APP_UPGRADE" = "Accept & Upgrade"; 15 | "i18n:installer.EULA_PROMPT_NOTICE" = "By installing the software, you must accept the terms above."; 16 | "i18n:installer.INSTALLER_APP_TITLE" = "ilimi Installer"; 17 | "i18n:installer.INSTALLER_APP_TITLE_FULL" = "ilimi Installer"; 18 | "i18n:installer.LICENSE_TITLE" = "License:"; 19 | "i18n:installer.REPO_URL_TEXT" = "https://github.com/y1lichen/ilimi-inputmethod"; 20 | "i18n:installer.STOPPING_THE_OLD_VERSION" = "Stopping the old version. This may take up to one minute…"; 21 | "ilimi Input Method" = "ilimi Input Method"; 22 | "ilimi is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "ilimi is ready to use. \n\nPlease relogin if this is the first time you install it in this user account."; 23 | "ilimi is upgraded, but please log out or reboot for the new version to be fully functional." = "ilimi is upgraded, but please log out or reboot for the new version to be fully functional."; 24 | "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources."; 25 | "Install Failed" = "Install Failed"; 26 | "Installation Successful" = "Installation Successful"; 27 | "OK" = "OK"; 28 | "Stopping the old version. This may take up to one minute…" = "Stopping the old version. This may take up to one minute…"; 29 | "Warning" = "Warning"; 30 | -------------------------------------------------------------------------------- /ilimiInstaller/ilimiInstaller.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ilimiInstaller/ilimiInstallerApp.swift: -------------------------------------------------------------------------------- 1 | // (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 2 | // ==================== 3 | // This code is released under the 3-Clause BSD license (SPDX-License-Identifier: BSD-3-Clause) 4 | 5 | import AppKit 6 | import SwiftUI 7 | 8 | @main 9 | struct ilimiInstallerApp: App { 10 | var body: some Scene { 11 | WindowGroup { 12 | ZStack(alignment: .center) { 13 | LinearGradient( 14 | gradient: Gradient( 15 | colors: [ 16 | Color(red: 0, green: 0, blue: 0xF4 / 255), 17 | .black, 18 | ] 19 | ), 20 | startPoint: .top, endPoint: .bottom 21 | ).overlay(alignment: .topLeading) { 22 | Text("ilimi Input Method") 23 | .font(.system(size: 30)) 24 | .italic().bold() 25 | .padding() 26 | .foregroundStyle(Color.white) 27 | .shadow(color: .black, radius: 0, x: 5, y: 5) 28 | } 29 | MainView() 30 | .shadow(color: .black, radius: 3, x: 0, y: 0) 31 | }.frame(width: 1000, height: 630) 32 | .onAppear { 33 | NSWindow.allowsAutomaticWindowTabbing = false 34 | for window in NSApp.windows { 35 | window.titlebarAppearsTransparent = true 36 | window.setContentSize(.init(width: 1000, height: 630)) 37 | window.standardWindowButton(.closeButton)?.isHidden = true 38 | window.standardWindowButton(.miniaturizeButton)?.isHidden = true 39 | window.standardWindowButton(.zoomButton)?.isHidden = true 40 | window.styleMask.remove(.resizable) 41 | window.orderFront(self) 42 | } 43 | } 44 | .onDisappear { 45 | NSApp.terminate(self) 46 | } 47 | } 48 | .commands { 49 | CommandGroup(replacing: .newItem) {} 50 | CommandGroup(replacing: .appInfo) {} 51 | CommandGroup(replacing: .help) {} 52 | CommandGroup(replacing: .appVisibility) {} 53 | CommandGroup(replacing: .systemServices) {} 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ilimiInstaller/zh-Hant.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | CFBundleName = "一粒米輸入法安裝程式"; 4 | NSHumanReadableCopyright = "© 2022 and onwards The ilimi-IME Project (3-Clause BSD license)."; 5 | CFEULAContent = "(c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license).\n\n這份授權條款,在使用者符合以下四條件的情形下,授予使用者使用及再散播本\n軟體套件裝原始碼及二進位可執行形式的權利,無論此包裝是否經改作皆然:\n\n1. 對於本軟體原始碼的再散播,必須保留上述的著作權宣告、此四條件表列,以\n 及下述的免責聲明。\n2. 對於本套件二進位可執行形式的再散播,必須連帶以檔案以及/或者其他附\n 於散播包裝中的媒介方式,重製上述之著作權宣告、此四條件表列,以及下述\n 的免責聲明。\n3. 未獲事前取得書面授權,不得使用著作權持有者或本軟體貢獻者之名稱,\n 來為本軟體之衍生物做任何表示支援、認可或推廣、促銷之行為。\n\n免責聲明:本軟體由著作權持有者及本軟體之貢獻者以現狀(as is)提供,\n本軟體套件裝不負任何明示或默示之擔保責任,包括但不限於就適售性以及特定目\n的的適用性為默示性擔保。著作權持有者及本軟體之貢獻者,無論任何條件、\n無論成因或任何責任主義、無論此責任為因合約關係、無過失責任主義或因非違\n約之侵權(包括過失或其他原因等)而起,對於任何因使用本軟體套件裝所產生的\n任何直接性、間接性、偶發性、特殊性、懲罰性或任何結果的損害(包括但不限\n於替代商品或勞務之購用、使用損失、資料損失、利益損失、業務中斷等等),\n不負任何責任,即在該種使用已獲事前告知可能會造成此類損害的情形下亦然。"; 6 | -------------------------------------------------------------------------------- /ilimiInstaller/zh-Hant.lproj/Installer-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | MBIN 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | CFEULAContent 24 | (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license).\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS IS&quot;\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSHasLocalizedDisplayName 28 | 29 | LSMinimumSystemVersion 30 | ${MACOSX_DEPLOYMENT_TARGET} 31 | NSHumanReadableCopyright 32 | (c) 2022 and onwards The ilimi-IME Project (3-Clause BSD license). 33 | NSMainNibFile 34 | MainMenu 35 | NSPrincipalClass 36 | NSApplication 37 | 38 | 39 | -------------------------------------------------------------------------------- /ilimiInstaller/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "Abort" = "放棄安裝"; 2 | "Attention" = "請注意"; 3 | "Cancel" = "取消"; 4 | "Cannot activate the input method." = "無法啟用輸入法。"; 5 | "Cannot copy the file to the destination." = "無法將輸入法拷貝至目的地。"; 6 | "Cannot find input source %@ after registration." = "在註冊完輸入法 \"%@\" 之後仍然無法找到該輸入法。"; 7 | "Cannot register input source %@ at %@." = "無法從檔案位置 %2$@ 安裝輸入法 \"%1$@\"。"; 8 | "Continue" = "繼續"; 9 | "Fatal Error" = "安裝錯誤"; 10 | "i18n:installer.ACCEPT_INSTALLATION" = "我接受"; 11 | "i18n:installer.APP_NAME" = "ilimi for macOS"; 12 | "i18n:installer.CANCEL_INSTALLATION" = "取消安裝"; 13 | "i18n:installer.DEV_CREW" = "一粒米輸入法 macOS 程式研發:Jefferson Chen、Shiki Suen(僅協力)。"; 14 | "i18n:installer.DO_APP_UPGRADE" = "接受並升級"; 15 | "i18n:installer.EULA_PROMPT_NOTICE" = "若要安裝該軟體,請接受上述條款。"; 16 | "i18n:installer.INSTALLER_APP_TITLE" = "一粒米輸入法安裝程式"; 17 | "i18n:installer.INSTALLER_APP_TITLE_FULL" = "一粒米輸入法安裝程式"; 18 | "i18n:installer.LICENSE_TITLE" = "授權合約:"; 19 | "i18n:installer.REPO_URL_TEXT" = "https://github.com/y1lichen/ilimi-inputmethod"; 20 | "i18n:installer.STOPPING_THE_OLD_VERSION" = "等待舊版完全停用,大約需要一分鐘…"; 21 | "ilimi Input Method" = "一粒米輸入法"; 22 | "ilimi is ready to use. \n\nPlease relogin if this is the first time you install it in this user account." = "一粒米輸入法安裝成功。\n\n若是在當前使用者帳戶內首次安裝的話,請重新登入。"; 23 | "ilimi is upgraded, but please log out or reboot for the new version to be fully functional." = "ilimi 安裝完成,但建議您登出或重新開機,以便順利使用新版。"; 24 | "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "輸入法已經安裝好,但可能沒有完全啟用。請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。"; 25 | "Install Failed" = "安裝失敗"; 26 | "Installation Successful" = "安裝成功"; 27 | "OK" = "確定"; 28 | "Stopping the old version. This may take up to one minute…" = "正在試圖結束正在運行的舊版輸入法,大概需要一分鐘…"; 29 | "Warning" = "安裝不完整"; 30 | -------------------------------------------------------------------------------- /media/ascii_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/media/ascii_demo.gif -------------------------------------------------------------------------------- /media/custom_phrase_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/media/custom_phrase_demo.png -------------------------------------------------------------------------------- /media/demo01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/media/demo01.gif -------------------------------------------------------------------------------- /media/demo02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/media/demo02.gif -------------------------------------------------------------------------------- /media/demo03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/media/demo03.gif -------------------------------------------------------------------------------- /media/demo04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/media/demo04.gif -------------------------------------------------------------------------------- /media/zhuyin_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/media/zhuyin_demo.gif -------------------------------------------------------------------------------- /others/image_assets/AppIcon-ilimi-RAW.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/others/image_assets/AppIcon-ilimi-RAW.heic -------------------------------------------------------------------------------- /others/image_assets/AppIcon-ilimi.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y1lichen/ilimi-inputmethod/7de9646807ef6e13633651c0b7249f29c3fc0ceb/others/image_assets/AppIcon-ilimi.pxd -------------------------------------------------------------------------------- /others/pinyin_txt_to_json.py: -------------------------------------------------------------------------------- 1 | # 將肥米輸入法的注音字典轉為json檔案 2 | import json 3 | 4 | # 初始化注符關係字典 5 | def init_dict(r1, r2): 6 | key_list = r1.split(' ') 7 | bpmf_list = r2.split(' ') 8 | key_bpmf_dict = {} 9 | for i in range(len(key_list)): 10 | key_bpmf_dict[key_list[i]] = bpmf_list[i] 11 | return key_bpmf_dict 12 | 13 | # 14 | def create_json_by_line(dict, row): 15 | elements_list = row.split(' ') 16 | chars = [] 17 | for i in range(1, len(elements_list)): 18 | chars.append(elements_list[i]) 19 | dict[elements_list[0]] = chars 20 | 21 | 22 | with open('./pinyin.txt') as f: 23 | # skip first line 24 | f.readline() 25 | r1 = f.readline().rstrip() 26 | r2 = f.readline().rstrip() 27 | key_bpmf_dict = init_dict(r1, r2) 28 | json_dict = {} 29 | line = f.readline() 30 | while line: 31 | create_json_by_line(json_dict, line.strip()) 32 | line = f.readline() 33 | filename = 'pinyin.json' 34 | with open(filename, 'w') as f: 35 | json.dump(json_dict, f, ensure_ascii=False, indent=4) 36 | --------------------------------------------------------------------------------