├── .github ├── CONTRIBUTING.md └── workflows │ ├── test.yml │ └── update.yml ├── .gitignore ├── CHANGELOG.md ├── FetchUpdateService ├── FetchUpdateService.entitlements ├── FetchUpdateService.swift ├── FetchUpdateServiceProtocol.swift ├── Info.plist └── main.swift ├── LICENSE ├── Makefile ├── README.md ├── SKKServClient ├── Info.plist ├── Network │ ├── Message+SKKServ.swift │ ├── NWParameters+SKKServ.swift │ ├── SKKServProtocol.swift │ └── SKKServRequest.swift ├── SKKServClient.entitlements ├── SKKServClient.swift ├── SKKServClientProtocol.swift └── main.swift ├── build_restart.sh ├── macSKK.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ └── macSKK.xcscheme ├── macSKK ├── Action.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-128.png │ │ ├── Icon-128@2x.png │ │ ├── Icon-16.png │ │ ├── Icon-16@2x.png │ │ ├── Icon-256.png │ │ ├── Icon-256@2x.png │ │ ├── Icon-32.png │ │ └── Icon-32@2x.png │ ├── Contents.json │ ├── icon-direct-locked.imageset │ │ ├── Contents.json │ │ ├── icon-direct-locked.png │ │ └── icon-direct-locked@2x.png │ ├── icon-direct.imageset │ │ ├── Contents.json │ │ ├── icon-direct.png │ │ └── icon-direct@2x.png │ ├── icon-eisu-locked.imageset │ │ ├── Contents.json │ │ ├── icon-eisu-locked.png │ │ └── icon-eisu-locked@2x.png │ ├── icon-eisu.imageset │ │ ├── Contents.json │ │ ├── icon-eisu.png │ │ └── icon-eisu@2x.png │ ├── icon-hankaku-locked.imageset │ │ ├── Contents.json │ │ ├── icon-hankaku-locked.png │ │ └── icon-hankaku-locked@2x.png │ ├── icon-hankaku.imageset │ │ ├── Contents.json │ │ ├── icon-hankaku.png │ │ └── icon-hankaku@2x.png │ ├── icon-hiragana-locked.imageset │ │ ├── Contents.json │ │ ├── icon-hiragana-locked.png │ │ └── icon-hiragana-locked@2x.png │ ├── icon-hiragana.imageset │ │ ├── Contents.json │ │ ├── icon-hiragana.png │ │ └── icon-hiragana@2x.png │ ├── icon-katakana-locked.imageset │ │ ├── Contents.json │ │ ├── icon-katakana-locked.png │ │ └── icon-katakana-locked@2x.png │ └── icon-katakana.imageset │ │ ├── Contents.json │ │ ├── icon-katakana.png │ │ └── icon-katakana@2x.png ├── Candidate.swift ├── Character+Additions.swift ├── Credits.rtf ├── CurrentInput.swift ├── Data+EucJis2004.swift ├── Dict.swift ├── DictionaryServiceExtention.h ├── Entry.swift ├── FileDict.swift ├── Global.swift ├── Info.plist ├── InfoPlist.strings ├── InputController.swift ├── InputSource.swift ├── Key.swift ├── KeyBinding.swift ├── KeyBindingSet.swift ├── LatestReleaseFetcher.swift ├── MarkedText.swift ├── MemoryDict.swift ├── NSEvent+CoreService.swift ├── NumberEntry.swift ├── Pasteboard.swift ├── Preview Content │ ├── Preview Assets.xcassets │ │ └── Contents.json │ └── SKK-JISYO.sample.utf-8 ├── Punctuation.swift ├── Release+UNNotification.swift ├── Release.swift ├── ReleaseVersion.swift ├── Romaji.swift ├── SKKServDict.swift ├── SKKServService.swift ├── Settings │ ├── DictionariesView.swift │ ├── DictionaryView.swift │ ├── DirectModeView.swift │ ├── GeneralView.swift │ ├── KeyBinding │ │ ├── KeyBindingInputsView.swift │ │ ├── KeyBindingSetView.swift │ │ └── KeyBindingView.swift │ ├── KeyEventView.swift │ ├── LogView.swift │ ├── SKKServDictView.swift │ ├── SettingsView.swift │ ├── SettingsViewModel.swift │ ├── SoftwareUpdateView.swift │ ├── SystemDictView.swift │ ├── UserDefaultsKeys.swift │ ├── WorkaroundApplicationView.swift │ └── WorkaroundView.swift ├── SettingsWatcher.swift ├── State.swift ├── StateMachine.swift ├── String+Transform.swift ├── SystemDict.swift ├── UNNotifier.swift ├── URL+Additions.swift ├── UpdateChecker.swift ├── UserDict.swift ├── UserNotificationDelegate.swift ├── View │ ├── AnnotationView.swift │ ├── CandidatesPanel.swift │ ├── CandidatesView.swift │ ├── CandidatesViewModel.swift │ ├── CompletionPanel.swift │ ├── CompletionView.swift │ ├── CompletionViewModel.swift │ ├── HorizontalCandidatesView.swift │ ├── InputModePanel.swift │ ├── SettingsWindow.swift │ └── VerticalCandidatesView.swift ├── Word.swift ├── direct.tiff ├── eisu.tiff ├── en.lproj │ └── Localizable.strings ├── hankaku.tiff ├── hiragana.tiff ├── ja.lproj │ └── Localizable.strings ├── kana-rule-azik.conf ├── kana-rule.conf ├── katakana.tiff ├── macSKK-Bridging-Header.h ├── macSKK.entitlements └── macSKKApp.swift ├── macSKKTests ├── CandidateTest.swift ├── Character+AdditionsTests.swift ├── Character+KeyCode.swift ├── CurrentInputTests.swift ├── Data+EucJis2004Tests.swift ├── EntryTests.swift ├── FileDictTests.swift ├── KeyBindingSetTests.swift ├── KeyBindingTests.swift ├── KeyTests.swift ├── MemoryDictTests.swift ├── NumberEntryTests.swift ├── PunctuationTests.swift ├── ReleaseVersionTests.swift ├── RomajiTests.swift ├── SKKServDictTests.swift ├── StateMachineTests.swift ├── StateTests.swift ├── String+TransformTests.swift ├── UpdateCheckerTests.swift ├── UserDict+Utilities.swift ├── UserDictTests.swift └── fixture │ ├── SKK-JISYO.broken.json │ ├── SKK-JISYO.test.json │ ├── SKK-JISYO.test.utf8 │ ├── empty.txt │ ├── euc-jis-2004.txt │ ├── kana-rule-for-test.conf │ ├── release.json │ └── utf8-bom.txt └── script ├── LICENSE.rtf ├── app.plist ├── dict.plist ├── distribution.xml.template ├── export-options.plist ├── scripts └── postinstall └── welcome.rtf /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | > [!WARNING] 4 | > macOS 14以降ではApp Sandboxの制限が強くなりました。すでにリリース版macSKKを使っている環境で開発版のmacSKKを使用すると起動時に `「"macSKK"がほかのアプリからのデータへのアクセスを求めています。」` というダイアログが表示されることがあります。これはリリース版で署名に使用しているTeam IDと異なるProvisioning Profileを使用している (もしくはAd hoc署名を使っている) 場合に同じユーザー辞書ファイルにアクセスすることで発生します。この状態で「許可」を選んでしまうとリリース版のmacSKKが逆に読み込めなくなるなどの想定しない問題が発生する可能性があります。事前にユーザー辞書をバックアップしておくことを推奨します。開発時のみBundle Identifierを変更することも検討してください。 5 | 6 | ## ローカルでのビルドと実行 7 | 8 | 手元で修正を加えたあと、Gitリポジトリのルートディレクトリにある`build_restart.sh`を実行することで使用するmacSKKをローカルビルドに差し替えします。 9 | 10 | ```console 11 | $ ./build_restart.sh 12 | ``` 13 | 14 | このスクリプトの実行により 15 | 16 | 1. ローカルのmacSKKがビルドされ 17 | 2. `~/Library/Input Methods/macSKK.app`が配置され 18 | 3. 既存のmacSKKのプロセスをkillして再起動 19 | 20 | 👆の3つが実行され、実行したPCで開発中のバージョンを試すことができます。 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '*.md' 8 | - '.github/workflows/update.yml' 9 | pull_request: 10 | branches: [main] 11 | paths-ignore: 12 | - '*.md' 13 | - '.github/workflows/update.yml' 14 | 15 | # bashを使うようにしてpipefailを有効にする 16 | # https://docs.github.com/ja/actions/writing-workflows/workflow-syntax-for-github-actions#defaultsrunshell 17 | defaults: 18 | run: 19 | shell: bash 20 | 21 | jobs: 22 | test: 23 | runs-on: macos-15 24 | steps: 25 | # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#xcode 26 | - name: Select Xcode version 27 | run: sudo xcode-select -s '/Applications/Xcode_16.3.app/Contents/Developer' 28 | - uses: actions/checkout@v4 29 | - name: test 30 | run: | 31 | xcodebuild -target macSKKTests -scheme macSKK DEVELOPMENT_TEAM= test | xcbeautify 32 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: update 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | # bashを使うようにしてpipefailを有効にする 8 | # https://docs.github.com/ja/actions/writing-workflows/workflow-syntax-for-github-actions#defaultsrunshell 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | update: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: calculate sha256 18 | id: sha256 19 | run: | 20 | browser_download_url=$(echo '${{ toJSON(github.event.release.assets) }}' | jq -r '.[] | select(.name | endswith(".dmg")) | .browser_download_url') 21 | SHA256=$(curl --fail -L "${browser_download_url}" | sha256sum | cut -d ' ' -f 1) 22 | echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" 23 | - name: dispatch 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.HOMEBREW_MACSKK_TOKEN }} 26 | run: | 27 | curl \ 28 | --fail \ 29 | -H "Authorization: token $GITHUB_TOKEN" \ 30 | -H "Accept: application/vnd.github.v3+json" \ 31 | -H "Content-Type: application/json" \ 32 | https://api.github.com/repos/mtgto/homebrew-macSKK/dispatches \ 33 | -d '{"event_type":"update","client_payload":{"version":"${{ github.event.release.tag_name }}","sha256":"${{ steps.sha256.outputs.sha256 }}"}}' 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | 69 | # Installer 70 | /script/work/ 71 | -------------------------------------------------------------------------------- /FetchUpdateService/FetchUpdateService.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /FetchUpdateService/FetchUpdateService.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | import os 6 | 7 | // Console.appで見るときにまとめて見れるようにsubsystemはアプリと同じのほうがよいかも? (categoryを変えるとか) 8 | let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "main") 9 | 10 | class FetchUpdateService: NSObject, FetchUpdateServiceProtocol { 11 | /** 12 | * GitHub APIで最新のリリースを取得する。 13 | * APIドキュメントにもあるようにパブリックリソースのみの場合は認証不要で取得できる。 14 | * https://docs.github.com/ja/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release 15 | */ 16 | @objc func fetch() async throws -> Data { 17 | var request = URLRequest(url: URL(string: "https://api.github.com/repos/mtgto/macSKK/releases/latest")!) 18 | request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") 19 | request.setValue("2022-11-28", forHTTPHeaderField: "X-GitHub-Api-Version") 20 | URLSession.shared.dataTask(with: request) 21 | do { 22 | let (data, response) = try await URLSession.shared.data(for: request) 23 | guard let response = response as? HTTPURLResponse else { 24 | fatalError("HTTPURLResponseになっていない") 25 | } 26 | if response.statusCode != 200 { 27 | logger.info("最新リリースの情報の取得時のHTTPステータスが \(response.statusCode) でした") 28 | throw FetchUpdateServiceError.invalidResponse 29 | } 30 | return data 31 | } catch { 32 | throw FetchUpdateServiceError.network 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /FetchUpdateService/FetchUpdateServiceProtocol.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | public enum FetchUpdateServiceError: Error { 7 | case invalidProxy // remoteObjectProxyが想定したプロトコルを満たしていない 8 | case invalidResponse 9 | case network 10 | } 11 | 12 | @objc protocol FetchUpdateServiceProtocol { 13 | /// GitHubのリリースページからリリース情報を取得してAtom (XML) を返す 14 | func fetch() async throws -> Data 15 | } 16 | -------------------------------------------------------------------------------- /FetchUpdateService/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | XPCService 6 | 7 | ServiceType 8 | Application 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /FetchUpdateService/main.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | class ServiceDelegate: NSObject, NSXPCListenerDelegate { 7 | 8 | /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. 9 | func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { 10 | 11 | // Configure the connection. 12 | // First, set the interface that the exported object implements. 13 | newConnection.exportedInterface = NSXPCInterface(with: FetchUpdateServiceProtocol.self) 14 | 15 | // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. 16 | let exportedObject = FetchUpdateService() 17 | newConnection.exportedObject = exportedObject 18 | 19 | // Resuming the connection allows the system to deliver more incoming messages. 20 | newConnection.resume() 21 | 22 | // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false. 23 | return true 24 | } 25 | } 26 | 27 | // Create the delegate for the service. 28 | let delegate = ServiceDelegate() 29 | 30 | // Set up the one NSXPCListener for this service. It will handle all incoming connections. 31 | let listener = NSXPCListener.service() 32 | listener.delegate = delegate 33 | 34 | // Resuming the serviceListener starts this service. This method does not return. 35 | listener.resume() 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # インストーラ作成Makefile 2 | # 公証に必要なので先に "Developer ID Application" のCertificateを生成してインストールしておくこと。 3 | # pbxprojファイルからバージョン (MARKETING_VERSION) を取得するのにjqを使っているのでインストールしておくこと。 4 | # 公証のためのApp Passwordは先にKeychainに入れておくこと. 次のコマンドで実行できる 5 | # xcrun notarytool store-credentials $(CREDENTIALS_PROFILE) --apple-id $(APPLE_ID) --team-id $(APPLE_TEAM_ID) 6 | # See https://developer.apple.com/documentation/technotes/tn3147-migrating-to-the-latest-notarization-tool#Save-credentials-in-the-keychain 7 | 8 | # 設定項目 9 | #APPLE_ID := hogerappa@gmail.com 10 | APPLE_TEAM_ID := W3A6B7FDC7 11 | CREDENTIALS_PROFILE := macSKK 12 | VERSION := $(shell xcodebuild -project macSKK.xcodeproj -target macSKK -showBuildSettings -json | jq -r '.[0].buildSettings.MARKETING_VERSION') 13 | 14 | WORKDIR := script/work 15 | SCRIPTSDIR := script/scripts 16 | XCARCHIVE := $(WORKDIR)/macSKK-$(VERSION).xcarchive 17 | APP := "$(WORKDIR)/export/macSKK.app" 18 | DSYMS := $(XCARCHIVE)/dSYMs 19 | DICT := $(WORKDIR)/SKK-JISYO.L 20 | APP_PKG := $(WORKDIR)/app.pkg 21 | DICT_PKG := $(WORKDIR)/dict.pkg 22 | INSTALLER_PKG := $(WORKDIR)/pkg/macSKK-$(VERSION).pkg 23 | UNSIGNED_PKG := $(WORKDIR)/macSKK-unsigned-$(VERSION).pkg 24 | # 最終成果物 25 | TARGET_DMG := $(WORKDIR)/macSKK-$(VERSION).dmg 26 | # ビルド時に生成されたdSYM (XPCのdSYMを含む) 27 | TARGET_DSYM_ARCHIVE := $(WORKDIR)/macSKK-$(VERSION)-dSYMs.zip 28 | APP_PKG_ID := net.mtgto.inputmethod.macSKK.app 29 | DICT_PKG_ID := net.mtgto.inputmethod.macSKK.dict 30 | PRODUCT_SIGN_ID := "Developer ID Installer" 31 | 32 | .PHONY: all $(DICT) 33 | 34 | $(XCARCHIVE): 35 | xcodebuild -project macSKK.xcodeproj -scheme macSKK -configuration Release CODE_SIGN_IDENTITY="Developer ID Application" DEVELOPMENT_TEAM=$(APPLE_TEAM_ID) OTHER_CODE_SIGN_FLAGS="--timestamp --options=runtime" CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO CODE_SIGN_STYLE=Manual -archivePath $(XCARCHIVE) archive 36 | 37 | $(APP): $(XCARCHIVE) 38 | xcodebuild -exportArchive -archivePath $(XCARCHIVE) -exportOptionsPlist script/export-options.plist -exportPath $(WORKDIR)/export 39 | 40 | all: $(XCARCHIVE) 41 | 42 | $(DICT): 43 | $(eval DICT_DIGEST_LATEST := $(shell curl --silent https://skk-dev.github.io/dict/SKK-JISYO.L.gz.md5 | cut -w -f 1)) 44 | $(eval DICT_DIGEST := $(shell if [ -f $(WORKDIR)/SKK-JISYO.L.gz ]; then md5 -q $(WORKDIR)/SKK-JISYO.L.gz; else echo NA; fi)) 45 | if [ $(DICT_DIGEST) != $(DICT_DIGEST_LATEST) ]; then \ 46 | curl https://skk-dev.github.io/dict/SKK-JISYO.L.gz -o $(WORKDIR)/SKK-JISYO.L.gz; \ 47 | gzip --decompress --keep --force $(WORKDIR)/SKK-JISYO.L.gz; \ 48 | fi 49 | 50 | $(DICT_PKG): $(DICT) 51 | mkdir -p $(WORKDIR)/dict/Library/Containers/net.mtgto.inputmethod.macSKK/Data/Documents/Dictionaries 52 | cp $< $(WORKDIR)/dict/Library/Containers/net.mtgto.inputmethod.macSKK/Data/Documents/Dictionaries 53 | pkgbuild --root $(WORKDIR)/dict --component-plist script/dict.plist --identifier $(DICT_PKG_ID) --version $(VERSION) --install-location / $(DICT_PKG) 54 | 55 | $(APP_PKG): $(APP) 56 | mkdir -p $(WORKDIR)/app/Library/Input\ Methods 57 | cp -r $< $(WORKDIR)/app/Library/Input\ Methods 58 | pkgbuild --root $(WORKDIR)/app --component-plist script/app.plist --identifier $(APP_PKG_ID) --version $(VERSION) --install-location / --scripts $(SCRIPTSDIR) $(APP_PKG) 59 | 60 | $(INSTALLER_PKG): $(APP_PKG) $(DICT_PKG) 61 | mkdir -p $(WORKDIR)/pkg 62 | sed -e "s/%TITLE%/macSKK $(VERSION)/" script/distribution.xml.template > script/distribution.xml 63 | productbuild --distribution script/distribution.xml --resources script --package-path $(WORKDIR) $(UNSIGNED_PKG) 64 | productsign --sign $(PRODUCT_SIGN_ID) $(UNSIGNED_PKG) $(INSTALLER_PKG) 65 | # store-credentialsしてある場合はキーチェーンの情報を使うのでAPPLE_IDは不要。 66 | # 将来、公証をGitHub Actionsなどで実行することになったらApp Passwordを使うようにするかも。 67 | #xcrun notarytool submit $(INSTALLER_PKG) --team-id $(APPLE_TEAM_ID) --apple-id $(APPLE_ID) --wait 68 | xcrun notarytool submit $(INSTALLER_PKG) -p $(CREDENTIALS_PROFILE) --wait 69 | xcrun stapler staple $(INSTALLER_PKG) 70 | 71 | $(TARGET_DMG): $(INSTALLER_PKG) 72 | if [ -f $(TARGET_DMG) ]; then rm $(TARGET_DMG); fi 73 | cp LICENSE $(WORKDIR)/pkg 74 | hdiutil create -srcfolder $(WORKDIR)/pkg -volname macSKK-$(VERSION) $(TARGET_DMG) 75 | 76 | # `zip -r` するときの作業ディレクトリを指定できないので雑な相対指定をしている 77 | $(TARGET_DSYM_ARCHIVE): $(APP) 78 | rm -f $(TARGET_DSYM_ARCHIVE) 79 | pushd $(XCARCHIVE)/dSYMs; zip ../../../../$(TARGET_DSYM_ARCHIVE) -r .; popd 80 | 81 | release: $(TARGET_DMG) $(TARGET_DSYM_ARCHIVE) 82 | 83 | clean: 84 | rm -rf $(WORKDIR) build 85 | -------------------------------------------------------------------------------- /SKKServClient/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | XPCService 6 | 7 | ServiceType 8 | Application 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /SKKServClient/Network/Message+SKKServ.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | import Network 6 | 7 | fileprivate let requestKey = "request" 8 | fileprivate let responseKey = "response" 9 | 10 | extension NWProtocolFramer.Message { 11 | convenience init(request: SKKServRequest) { 12 | self.init(definition: SKKServProtocol.definition) 13 | self[requestKey] = request 14 | } 15 | 16 | convenience init(response: Data) { 17 | self.init(definition: SKKServProtocol.definition) 18 | self[responseKey] = response 19 | } 20 | 21 | var request: SKKServRequest? { 22 | self[requestKey] as? SKKServRequest 23 | } 24 | 25 | var response: Data? { 26 | self[responseKey] as? Data 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SKKServClient/Network/NWParameters+SKKServ.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Network 5 | 6 | extension NWParameters { 7 | static var skkserv: NWParameters { 8 | let tcpOptions = NWProtocolTCP.Options() 9 | tcpOptions.connectionTimeout = 1 10 | tcpOptions.enableKeepalive = true 11 | tcpOptions.keepaliveInterval = 30 12 | tcpOptions.keepaliveCount = 3 13 | let parameters = NWParameters(tls: nil, tcp: tcpOptions) 14 | let options = NWProtocolFramer.Options(definition: SKKServProtocol.definition) 15 | parameters.defaultProtocolStack.applicationProtocols = [options] 16 | // parameters.acceptLocalOnly = true 17 | return parameters 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SKKServClient/Network/SKKServProtocol.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | import Network 6 | 7 | final class SKKServProtocol: NWProtocolFramerImplementation { 8 | static let label: String = "skkserv" 9 | static let definition = NWProtocolFramer.Definition(implementation: SKKServProtocol.self) 10 | var lastRequest: SKKServRequest? 11 | 12 | required init(framer: NWProtocolFramer.Instance) {} 13 | 14 | func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { 15 | .ready 16 | } 17 | 18 | func handleInput(framer: NWProtocolFramer.Instance) -> Int { 19 | while true { 20 | var received: Data? = nil 21 | _ = framer.parseInput(minimumIncompleteLength: 1, maximumLength: 1024 * 1024) { buffer, isComplete -> Int in 22 | // 直前のリクエストがサーバーのバージョン要求、サーバーのホスト名とIPアドレスのリスト要求の場合はスペースが終端記号となりLFは送られない 23 | // NOTE: 2024-03-15現在、yaskkserv2はIPアドレスのリスト要求の場合、スペースが終端記号になっていない 24 | if let lastRequest, let buffer, let index = buffer.firstIndex(of: lastRequest.terminateCharacter) { 25 | buffer[0.. Bool { 53 | // FIXME: 終端信号を送信してもよさそう? 54 | return true 55 | } 56 | 57 | func cleanup(framer: NWProtocolFramer.Instance) {} 58 | } 59 | -------------------------------------------------------------------------------- /SKKServClient/Network/SKKServRequest.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | // end, request, version, hostは以下の資料を参考にしています 7 | // https://github.com/jj1bdx/dbskkd-cdb/blob/master/skk-server-protocol.md 8 | // midashiは以下の資料を参考にしています 9 | // https://ja.osdn.net/projects/pysocialskkserv/wiki/SKKServ 10 | enum SKKServRequest { 11 | case end 12 | case request(Data) // 見出し語をエンコードしたもの 13 | case version 14 | case host 15 | case completion(Data) // 読みをエンコードしたもの 16 | 17 | var data: Data { 18 | let lf: UInt8 = 0x0a 19 | let space: UInt8 = 0x20 20 | 21 | switch self { 22 | case .end: 23 | return Data([0x30, space, lf]) // '0' + space + LF 24 | case .request(let key): 25 | var data = Data([0x31]) // '1' + key + space + LF 26 | data.append(key) 27 | data.append(contentsOf: [space, lf]) 28 | return data 29 | case .version: 30 | return Data([0x32, space, lf]) // '2' + space + LF 31 | case .host: 32 | return Data([0x33, space, lf]) // '3' + space + LF 33 | case .completion(let key): 34 | var data = Data([0x34]) // '4' + key + space + LF 35 | data.append(key) 36 | data.append(contentsOf: [space, lf]) 37 | return data 38 | } 39 | } 40 | 41 | var terminateCharacter: UInt8 { 42 | switch self { 43 | case .request, .completion: 44 | return 0x0a 45 | case .version, .host: 46 | return 0x20 47 | default: // .end のときは応答がない 48 | return 0 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SKKServClient/SKKServClient.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SKKServClient/SKKServClientProtocol.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | import Network 6 | 7 | public enum SKKServClientError: Error, CaseIterable { 8 | /// remoteObjectProxyが想定したプロトコルを満たしていないなど想定外のエラー 9 | case unexpected 10 | /// skkservと接続失敗した 11 | case connectionRefused 12 | /// skkservが仕様外のレスポンスを返した 13 | case invalidResponse 14 | /// 接続タイムアウト 15 | case connectionTimeout 16 | /// タイムアウト (接続タイムアウトは発生しなかったが応答が一定時間なかった) 17 | case timeout 18 | } 19 | 20 | @objc(SKKServDestination) public final class SKKServDestination: NSObject, NSSecureCoding, Sendable { 21 | public static let supportsSecureCoding: Bool = true 22 | 23 | let host: String 24 | let port: UInt16 25 | let encoding: String.Encoding 26 | 27 | init(host: String, port: UInt16, encoding: String.Encoding) { 28 | self.host = host 29 | self.port = port 30 | self.encoding = encoding 31 | } 32 | 33 | public required init?(coder: NSCoder) { 34 | guard let host = coder.decodeObject(of: NSString.self, forKey: "host") as? String else { return nil } 35 | self.host = host 36 | guard let port = coder.decodeObject(of: NSNumber.self, forKey: "port") else { return nil } 37 | self.port = port.uint16Value 38 | guard let encoding = coder.decodeObject(of: NSNumber.self, forKey: "encoding") else { return nil } 39 | self.encoding = String.Encoding(rawValue: encoding.uintValue) 40 | } 41 | 42 | var endpoint: NWEndpoint { 43 | NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(integerLiteral: port)) 44 | } 45 | 46 | // MARK: NSSecureCoding 47 | public func encode(with coder: NSCoder) { 48 | coder.encode(host, forKey: "host") 49 | coder.encode(NSNumber(value: port), forKey: "port") 50 | coder.encode(NSNumber(value: encoding.rawValue), forKey: "encoding") 51 | } 52 | 53 | // MARK: NSObject 54 | public override var hash: Int { 55 | var hasher = Hasher() 56 | hasher.combine(host) 57 | hasher.combine(port) 58 | hasher.combine(encoding) 59 | return hasher.finalize() 60 | } 61 | } 62 | 63 | @objc protocol SKKServClientProtocol { 64 | func serverVersion(destination: SKKServDestination, with reply: @escaping (String?, (any Error)?) -> Void) 65 | func refer(destination: SKKServDestination, yomi: String, with reply: @escaping (String?, (any Error)?) -> Void) 66 | func completion(destination: SKKServDestination, yomi: String, with reply: @escaping (String?, (any Error)?) -> Void) 67 | func disconnect() 68 | } 69 | -------------------------------------------------------------------------------- /SKKServClient/main.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | class ServiceDelegate: NSObject, NSXPCListenerDelegate { 7 | 8 | /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. 9 | func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { 10 | 11 | // Configure the connection. 12 | // First, set the interface that the exported object implements. 13 | newConnection.exportedInterface = NSXPCInterface(with: SKKServClientProtocol.self) 14 | 15 | // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. 16 | let exportedObject = SKKServClient() 17 | newConnection.exportedObject = exportedObject 18 | 19 | // Resuming the connection allows the system to deliver more incoming messages. 20 | newConnection.resume() 21 | 22 | // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false. 23 | return true 24 | } 25 | } 26 | 27 | // Create the delegate for the service. 28 | let delegate = ServiceDelegate() 29 | 30 | // Set up the one NSXPCListener for this service. It will handle all incoming connections. 31 | let listener = NSXPCListener.service() 32 | listener.delegate = delegate 33 | 34 | // Resuming the serviceListener starts this service. This method does not return. 35 | listener.resume() 36 | -------------------------------------------------------------------------------- /build_restart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | # ビルド 5 | xcodebuild -workspace macSKK.xcodeproj/project.xcworkspace -scheme macSKK -configuration Debug DEVELOPMENT_TEAM= clean archive -archivePath build/archive.xcarchive 6 | # 上書き 7 | rm -rf ~/Library/Input\ Methods/macSKK.app 8 | cp -r build/archive.xcarchive/Products/Library/Input\ Methods/macSKK.app ~/Library/Input\ Methods/ 9 | # 再起動 10 | pkill "macSKK" 11 | -------------------------------------------------------------------------------- /macSKK.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macSKK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macSKK.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | SPDX-License-Identifier: GPL-3.0-or-later 7 | 8 | 9 | -------------------------------------------------------------------------------- /macSKK.xcodeproj/xcshareddata/xcschemes/macSKK.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 35 | 36 | 40 | 41 | 42 | 43 | 46 | 52 | 53 | 54 | 55 | 56 | 66 | 68 | 74 | 75 | 76 | 77 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /macSKK/Action.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Cocoa 5 | import InputMethodKit 6 | 7 | struct Action { 8 | let keyBind: KeyBinding.Action? 9 | /// キーイベント 10 | let event: NSEvent 11 | let textInput: (any IMKTextInput)? 12 | /// ``Romaji/convertKeyEvent(_:)`` によって変換されたNSEventから生成されたアクションかどうか. 13 | let treatAsAlphabet: Bool 14 | 15 | init(keyBind: KeyBinding.Action?, event: NSEvent, textInput: (any IMKTextInput)? = nil, treatAsAlphabet: Bool = false) { 16 | self.keyBind = keyBind 17 | self.event = event 18 | self.textInput = textInput 19 | self.treatAsAlphabet = treatAsAlphabet 20 | } 21 | 22 | func shiftIsPressed() -> Bool { 23 | return event.modifierFlags.contains(.shift) 24 | } 25 | 26 | func optionIsPressed() -> Bool { 27 | return event.modifierFlags.contains(.option) 28 | } 29 | 30 | /// Option-Shift-E (´) のように入力したキーコードを元に整形された文字列を返す 31 | func characters() -> String? { 32 | return event.characters 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /macSKK/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Cocoa 5 | 6 | final class AppDelegate: NSObject, NSApplicationDelegate { 7 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { 8 | logger.log("アプリケーションが終了する前にユーザー辞書の永続化を行います") 9 | Global.dictionary.save() 10 | return .terminateNow 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /macSKK/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 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "idiom" : "mac", 53 | "scale" : "1x", 54 | "size" : "512x512" 55 | }, 56 | { 57 | "idiom" : "mac", 58 | "scale" : "2x", 59 | "size" : "512x512" 60 | } 61 | ], 62 | "info" : { 63 | "author" : "xcode", 64 | "version" : 1 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/AppIcon.appiconset/Icon-128.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/AppIcon.appiconset/Icon-128@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/AppIcon.appiconset/Icon-16.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/AppIcon.appiconset/Icon-256.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/AppIcon.appiconset/Icon-256@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/AppIcon.appiconset/Icon-32.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-direct-locked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-direct-locked.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-direct-locked@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-direct-locked.imageset/icon-direct-locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-direct-locked.imageset/icon-direct-locked.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-direct-locked.imageset/icon-direct-locked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-direct-locked.imageset/icon-direct-locked@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-direct.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-direct.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-direct@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-direct.imageset/icon-direct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-direct.imageset/icon-direct.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-direct.imageset/icon-direct@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-direct.imageset/icon-direct@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-eisu-locked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-eisu-locked.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-eisu-locked@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-eisu-locked.imageset/icon-eisu-locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-eisu-locked.imageset/icon-eisu-locked.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-eisu-locked.imageset/icon-eisu-locked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-eisu-locked.imageset/icon-eisu-locked@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-eisu.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-eisu.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-eisu@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-eisu.imageset/icon-eisu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-eisu.imageset/icon-eisu.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-eisu.imageset/icon-eisu@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-eisu.imageset/icon-eisu@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hankaku-locked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-hankaku-locked.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-hankaku-locked@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hankaku-locked.imageset/icon-hankaku-locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-hankaku-locked.imageset/icon-hankaku-locked.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hankaku-locked.imageset/icon-hankaku-locked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-hankaku-locked.imageset/icon-hankaku-locked@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hankaku.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-hankaku.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-hankaku@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hankaku.imageset/icon-hankaku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-hankaku.imageset/icon-hankaku.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hankaku.imageset/icon-hankaku@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-hankaku.imageset/icon-hankaku@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hiragana-locked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-hiragana-locked.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-hiragana-locked@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hiragana-locked.imageset/icon-hiragana-locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-hiragana-locked.imageset/icon-hiragana-locked.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hiragana-locked.imageset/icon-hiragana-locked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-hiragana-locked.imageset/icon-hiragana-locked@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hiragana.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-hiragana.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-hiragana@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hiragana.imageset/icon-hiragana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-hiragana.imageset/icon-hiragana.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-hiragana.imageset/icon-hiragana@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-hiragana.imageset/icon-hiragana@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-katakana-locked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-katakana-locked.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-katakana-locked@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-katakana-locked.imageset/icon-katakana-locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-katakana-locked.imageset/icon-katakana-locked.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-katakana-locked.imageset/icon-katakana-locked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-katakana-locked.imageset/icon-katakana-locked@2x.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-katakana.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-katakana.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-katakana@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-katakana.imageset/icon-katakana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-katakana.imageset/icon-katakana.png -------------------------------------------------------------------------------- /macSKK/Assets.xcassets/icon-katakana.imageset/icon-katakana@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/Assets.xcassets/icon-katakana.imageset/icon-katakana@2x.png -------------------------------------------------------------------------------- /macSKK/Candidate.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | /** 7 | * 変換候補 8 | */ 9 | struct Candidate: Hashable { 10 | /** 11 | * 辞書上での表記 12 | */ 13 | struct Original: Hashable { 14 | /** 15 | * 辞書上の見出し語の表記。 16 | * 数値変換の場合 "だい#" のような数値部分を "#" で表す表記がされている。 17 | */ 18 | let midashi: String 19 | /** 20 | * 辞書上の変換結果の表記。 21 | * 数値変換の場合 "第#1" のような変換フォーマットを表す表記がされている。 22 | */ 23 | let word: Word.Word 24 | } 25 | 26 | /** 27 | * 変換結果。数値変換の場合は例外あり。 28 | * 数値変換の場合、辞書には "第#1" のように登録されているが "第5" のようにユーザー入力で置換されている。 29 | */ 30 | let word: Word.Word 31 | 32 | /** 33 | * 辞書上の表記。現在は数値変換時のみ設定される。 34 | */ 35 | let original: Original? 36 | 37 | /** 38 | * 注釈。複数の辞書によるものがあればまとめられている。 39 | */ 40 | private(set) var annotations: [Annotation] 41 | 42 | /** 43 | * 辞書に登録されている読み。 44 | */ 45 | func toMidashiString(yomi: String) -> String { 46 | original?.midashi ?? yomi 47 | } 48 | 49 | /** 50 | * 辞書に登録されている変換候補。 51 | */ 52 | var candidateString: String { 53 | original?.word ?? word 54 | } 55 | 56 | init(_ word: Word.Word, annotations: [Annotation] = [], original: Original? = nil) { 57 | self.word = word 58 | self.annotations = annotations 59 | self.original = original 60 | } 61 | 62 | func hash(into hasher: inout Hasher) { 63 | hasher.combine(word) 64 | } 65 | 66 | /// 注釈を追加する。すでに同じテキストをもつ注釈があれば追加されない。 67 | mutating func appendAnnotations(_ annotations: [Annotation]) { 68 | for annotation in annotations { 69 | if self.annotations.allSatisfy({ $0.text != annotation.text }) { 70 | self.annotations.append(annotation) 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /macSKK/Character+Additions.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | extension Character { 7 | /** 8 | * アルファベットで構成されているかを返す。 9 | */ 10 | var isAlphabet: Bool { 11 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".contains(self) 12 | } 13 | 14 | var isNumber: Bool { 15 | "0123456789".contains(self) 16 | } 17 | 18 | /** 19 | * ひらがなで構成されているかを返す。 20 | */ 21 | var isHiragana: Bool { 22 | guard let first = self.unicodeScalars.first else { return false } 23 | return 0x3041 <= first.value && first.value <= 0x309f 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /macSKK/CurrentInput.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import AppKit 5 | 6 | /** 7 | * 整理されたキー入力情報。 8 | * 9 | * NSEventからキーと修飾キーについてIMEの入力として必要な情報だけを取り出して所持しておく。 10 | */ 11 | struct CurrentInput: Equatable { 12 | let key: Key 13 | let modifierFlags: NSEvent.ModifierFlags 14 | 15 | init(key: Key, modifierFlags: NSEvent.ModifierFlags) { 16 | self.key = key 17 | self.modifierFlags = modifierFlags 18 | } 19 | 20 | /** 21 | * > Important: keyBindingInputsViewのキーイベントで取れるNSEventのcharactersIgnoringModifiersはシフトで変わる記号 (Shift-1で!など) 22 | * の場合、"!" になっている。そのようなNSEventの場合、本来をKey.characterとして解釈されるべきところで 23 | * Key.codeとして解釈されてしまうため使用しないこと。 24 | */ 25 | init(event: NSEvent) { 26 | if let character = event.charactersIgnoringModifiers?.lowercased().first, Key.characters.contains(character) { 27 | key = .character(character) 28 | } else { 29 | key = .code(event.keyCode) 30 | } 31 | 32 | // 使用する可能性があるものだけを抽出する。じゃないとrawValueで256が入ってしまうっぽい? 33 | modifierFlags = event.modifierFlags.intersection(Key.allowedModifierFlags) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /macSKK/Data+EucJis2004.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | enum EucJis2004Error: Error { 7 | case unsupported 8 | case convert 9 | } 10 | 11 | extension Data { 12 | /** 13 | * libiconvを使ってEUC-JPの拡張であるEUC-JISX0213としてデコードする。 14 | */ 15 | func eucJis2004String() throws -> String { 16 | if isEmpty { 17 | return "" 18 | } 19 | let cd = iconv_open("UTF-8".cString(using: .ascii), "EUC-JISX0213".cString(using: .ascii)) 20 | if cd == iconv_t(bitPattern: -1) { 21 | logger.error("iconvの初期化に失敗しました") 22 | throw EucJis2004Error.unsupported 23 | } 24 | defer { 25 | if iconv_close(cd) == -1 { 26 | logger.error("iconv変換ディスクリプタの解放に失敗しました: \(errno)") 27 | } 28 | } 29 | var data = self 30 | var inLeft = data.count 31 | // EUC-JIS-2004は1文字で1..2バイト (ASCIIは1バイト)、UTF-8は1..4バイト (ASCIIは1バイト) なのでバッファサイズは2倍用意する 32 | var outLeft = data.count * 2 33 | var buffer = Array(repeating: 0, count: outLeft) 34 | return try data.withUnsafeMutableBytes { 35 | var inPtr = $0.baseAddress?.assumingMemoryBound(to: CChar.self) 36 | try buffer.withUnsafeMutableBufferPointer { 37 | var outPtr = $0.baseAddress 38 | let ret = iconv(cd, &inPtr, &inLeft, &outPtr, &outLeft) 39 | if ret == -1 { 40 | if errno == EBADF { 41 | logger.error("iconv変換ディスクリプタの状態が異常です") 42 | } else if errno == EILSEQ { 43 | logger.error("入力に不正なバイト列が存在します") 44 | } else if errno == E2BIG { 45 | logger.error("EUC-JIS-2004からの変換先のバッファが足りません") 46 | } else if errno == EINVAL { 47 | logger.error("入力文字列が終端していません") 48 | } 49 | throw EucJis2004Error.convert 50 | } else if ret > 0 { 51 | logger.warning("EUC-JIS-2004から処理できない文字が \(ret) 文字ありました") 52 | } 53 | } 54 | guard let str = String(validatingUTF8: buffer) else { 55 | throw EucJis2004Error.convert 56 | } 57 | return str 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /macSKK/Dict.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | /// 辞書を引くときに指定する特殊なエントリの種類を指定するオプション 7 | enum DictReferringOption { 8 | /// 接頭辞を返す 9 | case prefix 10 | /// 接尾辞を返す 11 | case suffix 12 | /// 送り仮名ブロック。引数は例えば「大き」なら「き」、「行った」なら「った」のような送り仮名部分 13 | case okuri(String) 14 | } 15 | 16 | /// 辞書の読み込み状態 17 | enum DictLoadStatus { 18 | /// 正常に読み込み済み。引数は読み込めたエントリ数と読み込みできなかった行数 19 | case loaded(success: Int, failure: Int) 20 | case loading 21 | /// 無効に設定されている 22 | case disabled 23 | case fail(any Error) 24 | } 25 | 26 | /// 辞書の読み込み状態の通知オブジェクト 27 | struct DictLoadEvent { 28 | let id: FileDict.ID 29 | let status: DictLoadStatus 30 | } 31 | 32 | protocol DictProtocol { 33 | /** 34 | * 辞書を引き変換候補順に返す 35 | * 36 | * optionが設定されている場合は通常のエントリは検索しない 37 | * 38 | * - Parameters: 39 | * - yomi: SKK辞書の見出し。複数のひらがな、もしくは複数のひらがな + ローマ字からなる文字列 40 | * - option: 辞書を引くときに接頭辞、接尾辞や送り仮名ブロックから検索するかどうか。nilなら通常のエントリから検索する 41 | */ 42 | func refer(_ yomi: String, option: DictReferringOption?) -> [Word] 43 | 44 | /** 45 | * 辞書を逆引きし、最初に見つかった読みを返す 46 | */ 47 | func reverseRefer(_ word: String) -> String? 48 | 49 | /** 50 | * 辞書にエントリを追加する。 51 | * 52 | * - Parameters: 53 | * - yomi: SKK辞書の見出し。複数のひらがな、もしくは複数のひらがな + ローマ字からなる文字列 54 | * - word: SKK辞書の変換候補。 55 | */ 56 | mutating func add(yomi: String, word: Word) 57 | 58 | /** 59 | * 辞書からエントリを削除する。 60 | * 61 | * 辞書にないエントリ (ファイル辞書) の削除は無視されます。 62 | * 63 | * - Parameters: 64 | * - yomi: SKK辞書の見出し。複数のひらがな、もしくは複数のひらがな + ローマ字からなる文字列 65 | * - word: SKK辞書の変換候補。 66 | * - Returns: エントリを削除できたかどうか 67 | */ 68 | mutating func delete(yomi: String, word: Word.Word) -> Bool 69 | 70 | /** 71 | * 現在入力中のprefixに続く入力候補を1つ返す。見つからなければnilを返す。 72 | * 73 | * 以下のように補完候補を探します。 74 | * ※将来この仕様は変更する可能性が大いにあります。 75 | * 76 | * - prefixが空文字列ならnilを返す 77 | * - ユーザー辞書の送りなしの読みのうち、最近変換したものから選択する。 78 | * - prefixと読みが完全に一致する場合は補完候補とはしない 79 | * - 数値変換用の読みは補完候補としない 80 | */ 81 | func findCompletion(prefix: String) -> String? 82 | } 83 | -------------------------------------------------------------------------------- /macSKK/DictionaryServiceExtention.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #import 5 | 6 | // Dictionary.appで有効になっている辞書を返す 7 | NSArray * _Nonnull DCSGetActiveDictionaries(); 8 | // Dictionary.appで無効になっているものを含めて利用可能な辞書を返す 9 | NSSet * _Nonnull DCSCopyAvailableDictionaries(); 10 | NSString * _Nullable DCSDictionaryGetName(DCSDictionaryRef _Nullable dictID); 11 | NSString * _Nullable DCSDictionaryGetIdentifier(DCSDictionaryRef _Nullable dictID); 12 | -------------------------------------------------------------------------------- /macSKK/Global.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import AppKit 5 | import Combine 6 | 7 | /** 8 | * メインスレッドからのみ参照するグローバルな要素を保持する 9 | */ 10 | @MainActor struct Global { 11 | static let shared = Global() 12 | /// 利用可能な辞書の集合 13 | static var dictionary: UserDict! 14 | /// skkserv辞書 15 | static var skkservDict: SKKServDict? = nil 16 | static let privateMode = CurrentValueSubject(false) 17 | /// プライベートモード時に変換候補にユーザー辞書を無視するかどうか 18 | static var ignoreUserDictInPrivateMode = CurrentValueSubject(false) 19 | // 直接入力するアプリケーションのBundleIdentifierの集合のコピー。 20 | // マスターはSettingsViewModelがもっているが、InputControllerからAppが参照できないのでグローバル変数にコピーしている。 21 | // FIXME: NotificationCenter経由で設定画面で変更したことを各InputControllerに通知するようにしてこの変数は消すかも。 22 | static let directModeBundleIdentifiers = CurrentValueSubject<[String], Never>([]) 23 | /// モード変更時に空白文字を一瞬追加するワークアラウンドを適用するBundle Identifierの集合 24 | static let insertBlankStringBundleIdentifiers = CurrentValueSubject<[String], Never>([]) 25 | /// ユーザー辞書だけでなくすべての辞書から補完候補を検索するか? 26 | static let findCompletionFromAllDicts = CurrentValueSubject(false) 27 | /// 現在のローマ字かな変換ルール 28 | static var kanaRule: Romaji! 29 | /// デフォルトでもってるローマ字かな変換ルール 30 | static var defaultKanaRule: Romaji! 31 | /// 現在のキーバインディング 32 | static var keyBinding: KeyBindingSet = KeyBindingSet.defaultKeyBindingSet 33 | /// 変換候補パネルから選択するときに使用するキーの配列。英字の場合は小文字にしておくこと。 34 | static var selectCandidateKeys: [Character] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] 35 | /// Enterキーで変換候補の確定だけでなく改行も行うかどうか 36 | /// ddskkの `skk-egg-like-newline` やAquaSKKの `suppress_newline_on_commit` がfalseのときと同じ 37 | static var enterNewLine: Bool = false 38 | /// 補完候補を表示するか? 39 | static var showCompletion: Bool = true 40 | /// 注釈で使用するシステム辞書 41 | static var systemDict: SystemDict.Kind = .daijirin 42 | /// 変換候補選択中のバックスペースの挙動 43 | static var selectingBackspace: SelectingBackspace = .default 44 | /// カンマかピリオドを入力したときに入力する句読点の設定 45 | static var punctuation: Punctuation = .default 46 | /// 変換候補パネルの表示方向 47 | static var candidateListDirection = CurrentValueSubject(.vertical) 48 | /// 現在のモードを表示するパネル 49 | private let inputModePanel: InputModePanel 50 | /// 変換候補を表示するパネル 51 | private let candidatesPanel: CandidatesPanel 52 | /// 補完候補を表示するパネル 53 | private let completionPanel: CompletionPanel 54 | 55 | init() { 56 | inputModePanel = InputModePanel() 57 | candidatesPanel = CandidatesPanel( 58 | showAnnotationPopover: UserDefaults.standard.bool(forKey: UserDefaultsKeys.showAnnotation), 59 | candidatesFontSize: UserDefaults.standard.integer(forKey: UserDefaultsKeys.candidatesFontSize), 60 | annotationFontSize: UserDefaults.standard.integer(forKey: UserDefaultsKeys.annotationFontSize) 61 | ) 62 | completionPanel = CompletionPanel() 63 | } 64 | 65 | static var inputModePanel: InputModePanel { 66 | shared.inputModePanel 67 | } 68 | 69 | static var completionPanel: CompletionPanel { 70 | shared.completionPanel 71 | } 72 | 73 | static var candidatesPanel: CandidatesPanel { 74 | shared.candidatesPanel 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /macSKK/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | InputMethodConnectionName 6 | net.mtgto.inputmethod.macSKK_Connection 7 | InputMethodServerControllerClass 8 | InputController 9 | TISIntendedLanguage 10 | ja 11 | TISIconIsTemplate 12 | 13 | ComponentInputModeDict 14 | 15 | tsInputModeListKey 16 | 17 | net.mtgto.inputmethod.macSKK.ascii 18 | 19 | TISInputSourceID 20 | net.mtgto.inputmethod.macSKK.ascii 21 | TISIntendedLanguage 22 | en 23 | tsInputModeDefaultStateKey 24 | 25 | tsInputModeIsVisibleKey 26 | 27 | tsInputModeJISKeyboardShortcutKey 28 | 3 29 | tsInputModeMenuIconFileKey 30 | direct.tiff 31 | tsInputModePaletteIconFileKey 32 | direct.tiff 33 | tsInputModePrimaryInScriptKey 34 | 35 | tsInputModeScriptKey 36 | smRoman 37 | 38 | net.mtgto.inputmethod.macSKK.hiragana 39 | 40 | TISInputSourceID 41 | net.mtgto.inputmethod.macSKK.hiragana 42 | TISIntendedLanguage 43 | ja 44 | tsInputModeDefaultStateKey 45 | 46 | tsInputModeIsVisibleKey 47 | 48 | tsInputModeJISKeyboardShortcutKey 49 | 1 50 | tsInputModeMenuIconFileKey 51 | hiragana.tiff 52 | tsInputModePaletteIconFileKey 53 | hiragana.tiff 54 | tsInputModePrimaryInScriptKey 55 | 56 | tsInputModeScriptKey 57 | smJapanese 58 | 59 | net.mtgto.inputmethod.macSKK.katakana 60 | 61 | TISInputSourceID 62 | net.mtgto.inputmethod.macSKK.katakana 63 | TISIntendedLanguage 64 | ja 65 | tsInputModeDefaultStateKey 66 | 67 | tsInputModeIsVisibleKey 68 | 69 | tsInputModeJISKeyboardShortcutKey 70 | 2 71 | tsInputModeMenuIconFileKey 72 | katakana.tiff 73 | tsInputModePaletteIconFileKey 74 | katakana.tiff 75 | tsInputModePrimaryInScriptKey 76 | 77 | tsInputModeScriptKey 78 | smJapanese 79 | 80 | net.mtgto.inputmethod.macSKK.hankaku 81 | 82 | TISInputSourceID 83 | net.mtgto.inputmethod.macSKK.hankaku 84 | TISIntendedLanguage 85 | ja 86 | tsInputModeDefaultStateKey 87 | 88 | tsInputModeIsVisibleKey 89 | 90 | tsInputModeJISKeyboardShortcutKey 91 | 0 92 | tsInputModeMenuIconFileKey 93 | hankaku.tiff 94 | tsInputModePaletteIconFileKey 95 | hankaku.tiff 96 | tsInputModePrimaryInScriptKey 97 | 98 | tsInputModeScriptKey 99 | smJapanese 100 | 101 | net.mtgto.inputmethod.macSKK.eisu 102 | 103 | TISInputSourceID 104 | net.mtgto.inputmethod.macSKK.eisu 105 | TISIntendedLanguage 106 | ja 107 | tsInputModeDefaultStateKey 108 | 109 | tsInputModeIsVisibleKey 110 | 111 | tsInputModeJISKeyboardShortcutKey 112 | 0 113 | tsInputModeMenuIconFileKey 114 | eisu.tiff 115 | tsInputModePaletteIconFileKey 116 | eisu.tiff 117 | tsInputModePrimaryInScriptKey 118 | 119 | tsInputModeScriptKey 120 | smJapanese 121 | 122 | 123 | tsVisibleInputModeOrderedArrayKey 124 | 125 | net.mtgto.inputmethod.macSKK.ascii 126 | net.mtgto.inputmethod.macSKK.hiragana 127 | net.mtgto.inputmethod.macSKK.katakana 128 | net.mtgto.inputmethod.macSKK.hankaku 129 | net.mtgto.inputmethod.macSKK.eisu 130 | 131 | 132 | tsInputMethodCharacterRepertoireKey 133 | 134 | Hira 135 | Kana 136 | Latn 137 | 138 | tsInputMethodIconFileKey 139 | icon-hiragana.png 140 | 141 | 142 | -------------------------------------------------------------------------------- /macSKK/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | CFBundleName = "macSKK"; 2 | net.mtgto.inputmethod.macSKK.ascii = "ABC"; 3 | net.mtgto.inputmethod.macSKK.hiragana = "ひらがな"; 4 | net.mtgto.inputmethod.macSKK.katakana = "カタカナ"; 5 | net.mtgto.inputmethod.macSKK.hankaku = "半角カナ"; 6 | net.mtgto.inputmethod.macSKK.eisu = "全角英数"; 7 | -------------------------------------------------------------------------------- /macSKK/InputSource.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | import InputMethodKit 6 | 7 | /// キー配列 8 | struct InputSource: Hashable, Identifiable { 9 | // inputSourceID 10 | let id: String 11 | let localizedName: String 12 | // 初期値はQWERTY 13 | static let defaultInputSourceId = "com.apple.keylayout.ABC" 14 | 15 | // インストール済で利用可能なキー配列を取得する 16 | static func fetch() -> [InputSource]? { 17 | let options: [CFString: AnyObject] = [ 18 | kTISPropertyInputSourceType: kTISTypeKeyboardLayout, 19 | kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue, 20 | ] 21 | // APIドキュメントには特に書いてないけどCFGetRetainCountで見るとretainedな値を返してそうなのでtakeRetainedValueで変換 22 | guard let result = TISCreateInputSourceList(options as CFDictionary, true).takeRetainedValue() as? Array else { 23 | return nil 24 | } 25 | return result.compactMap { inputSource -> InputSource? in 26 | guard let id = getStringProperty(inputSource, key: kTISPropertyInputSourceID) else { return nil } 27 | guard let localizedName = getStringProperty(inputSource, key: kTISPropertyLocalizedName) else { return nil } 28 | // 第一言語が英語じゃないものは弾く。 29 | if let languagesPointer = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceLanguages), 30 | let languages = Unmanaged.fromOpaque(languagesPointer).takeUnretainedValue() as? Array { 31 | if let first = languages.first { 32 | if first != "en" { 33 | return nil 34 | } 35 | } 36 | } 37 | return InputSource(id: id, localizedName: localizedName) 38 | } 39 | } 40 | 41 | static func getStringProperty(_ tisInputSource: TISInputSource, key: NSString) -> String? { 42 | guard let pointer = TISGetInputSourceProperty(tisInputSource, key) else { return nil } 43 | return String(Unmanaged.fromOpaque(pointer).takeUnretainedValue()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /macSKK/Key.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import AppKit 5 | 6 | /** 7 | * macSKKのキー情報。 8 | */ 9 | enum Key: Hashable, Equatable { 10 | /// jやqやlなど、キーに印字されているテキスト。 11 | /// シフトを押しながら入力する場合は英字は小文字、!や#など記号はそのまま。 12 | case character(Character) 13 | /// keyCode形式。矢印キーなど表記できないキーを表現するために使用する。 14 | /// 設定でDvorak配列を選んでいる場合などもkeyCodeはQwerty配列の位置のままなので基本的にはcharacterで設定すること。 15 | /// 例えば設定でDvorak配列を選んだ状態でoを入力してもkeyCodeはQwerty配列のsのキーと同じになる。 16 | case code(UInt16) 17 | /// macSKKでkeyCodeベースでなく印字されている文字で取り扱うキーの集合。 18 | /// Shiftを押しながら入力する記号は含めない。これはIMKInputControllerに渡されるNSEventの 19 | /// NSEvent#charactersIgnoringModifiersと同じ。 20 | static let characters: [Character] = "abcdefghijklmnopqrstuvwxyz1234567890,./;-=`'\\@^[]".map { $0 } 21 | 22 | /// Inputで管理する修飾キーの集合。ここに含まれてない修飾キーは無視する 23 | static let allowedModifierFlags: NSEvent.ModifierFlags = [.shift, .control, .function, .option, .command] 24 | 25 | // UserDefaultsからのデコード用 26 | init?(rawValue: Any) { 27 | if let character = rawValue as? String { 28 | if Self.characters.contains(character) { 29 | self = .character(Character(character)) 30 | } else { 31 | logger.warning("キーバインドに使えない文字 \"\(character, privacy: .public)\" が指定されています") 32 | return nil 33 | } 34 | } else if let keyCode = rawValue as? UInt16 { 35 | self = .code(keyCode) 36 | } else { 37 | return nil 38 | } 39 | } 40 | 41 | // UserDefaultsへのエンコード用 42 | func encode() -> Any { 43 | switch self { 44 | case .character(let character): 45 | return String(character) 46 | case .code(let keyCode): 47 | return keyCode 48 | } 49 | } 50 | 51 | var displayString: String { 52 | switch self { 53 | case .character(let character): 54 | if character.isAlphabet { 55 | return character.uppercased() 56 | } else { 57 | return String(character) 58 | } 59 | case .code(let keyCode): 60 | switch keyCode { 61 | case 0x24: 62 | return "Enter" 63 | case 0x30: 64 | return "Tab" 65 | case 0x31: 66 | return "Space" 67 | case 0x33: 68 | return "Backspace" 69 | case 0x35: 70 | return "ESC" 71 | case 0x66: 72 | return String(localized: "KeyEisu") 73 | case 0x68: 74 | return String(localized: "KeyKana") 75 | case 0x73: 76 | return "Home" 77 | case 0x74: 78 | return "PageUp" 79 | case 0x75: 80 | return "Delete" 81 | case 0x77: 82 | return "End" 83 | case 0x79: 84 | return "PageDown" 85 | case 0x7b: 86 | return "←" 87 | case 0x7c: 88 | return "→" 89 | case 0x7d: 90 | return "↓" 91 | case 0x7e: 92 | return "↑" 93 | default: 94 | return "\(keyCode)" 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /macSKK/KeyBindingSet.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import AppKit 5 | 6 | struct KeyBindingSet: Identifiable, Hashable { 7 | /// 設定の名称。 8 | let id: String 9 | static let defaultId: ID = "macSKK" 10 | static let serializeVersion: Int = 1 11 | /** 12 | * 修飾キーを除いたキー入力が同じ場合は修飾キーが多いものが前に来るように並べた配列。 13 | * 入力に一番合致するキー入力を返すために最初にソートしてもっておく。 14 | */ 15 | let sorted: [(KeyBinding.Input, KeyBinding.Action)] 16 | 17 | // KeyBinding.Actionの順に並べたキーバインディングの配列 18 | var values: [KeyBinding] { 19 | let dict = Dictionary(grouping: sorted, by: { $0.1 }).mapValues { $0.map { $0.0 } } 20 | return KeyBinding.Action.allCases.map { action in 21 | KeyBinding(action, dict[action] ?? []) 22 | } 23 | } 24 | 25 | static let defaultKeyBindingSet = KeyBindingSet(id: Self.defaultId, values: KeyBinding.defaultKeyBindingSettings) 26 | 27 | init(id: String, values: [KeyBinding]) { 28 | self.id = id 29 | sorted = values.flatMap { keyValue in 30 | keyValue.inputs.map { ($0, keyValue.action) } 31 | }.sorted(by: { lts, rts in 32 | if lts.0.key == rts.0.key { 33 | return lts.0.modifierFlags.rawValue > rts.0.modifierFlags.rawValue 34 | } else { 35 | switch (lts.0.key, rts.0.key) { 36 | case let (.character(l), .character(r)): 37 | return l < r 38 | case let (.code(l), .code(r)): 39 | return l < r 40 | // .character, .codeはどういう順序で並んでいてもいいので、いったん`.code < .character`としておく。 41 | case (.character, .code): 42 | return false 43 | case (.code, .character): 44 | return true 45 | } 46 | } 47 | }) 48 | } 49 | 50 | // UserDefaultsからのデコード用 51 | init?(dict: [String: Any]) { 52 | guard let id = dict["id"] as? String, let keyBindings = dict["keyBindings"] as? [[String: Any]], let version = dict["version"] as? Int else { 53 | return nil 54 | } 55 | var values: [KeyBinding] = [] 56 | if version != Self.serializeVersion { 57 | logger.error("シリアライズバージョンが合わないためキーバインド \(id, privacy: .public) が読み込めません。(現在: \(Self.serializeVersion), 環境設定: \(version))") 58 | return nil 59 | } 60 | for dict in keyBindings { 61 | guard let keyBinding = KeyBinding(dict: dict) else { 62 | // 読み込めないKeyBindingは無視して次に進める。 63 | // 設定から更新すると読み込めなかったKeyBindingを除いて永続化される。 64 | logger.warning("キーバインド \(id, privacy: .public) に読み込めないエントリが見つかりました") 65 | continue 66 | } 67 | values.append(keyBinding) 68 | } 69 | // 不足しているキーバインドがあればデフォルト値を設定する 70 | KeyBinding.defaultKeyBindingSettings.forEach { keyBinding in 71 | if values.allSatisfy({ $0.action != keyBinding.action }) { 72 | values.append(keyBinding) 73 | } 74 | } 75 | self.init(id: id, values: values) 76 | } 77 | 78 | // UserDefaultsへのエンコード用 79 | func encode() -> [String: Any] { 80 | ["id": id, "version": Self.serializeVersion, "keyBindings": values.map { $0.encode() }] 81 | } 82 | 83 | private init(id: String, sorted: [(KeyBinding.Input, KeyBinding.Action)]) { 84 | self.id = id 85 | self.sorted = sorted 86 | } 87 | 88 | /// 現在のキーバインドに割り当てられているアクションを返す。 89 | /// 入力はIMKInputController#handleの引数のNSEventなので、charactersIgnoreingModifiersがシフトキーの影響を受けない。 90 | func action(event: NSEvent, inputMethodState: InputMethodState) -> KeyBinding.Action? { 91 | let currentInput = CurrentInput(event: event) 92 | 93 | return sorted.first(where: { 94 | $0.0.accepts(currentInput: currentInput) && $0.1.accepts(inputMethodState: inputMethodState) 95 | })?.1 96 | } 97 | 98 | var canDelete: Bool { 99 | id != Self.defaultId 100 | } 101 | 102 | var canEdit: Bool { 103 | id != Self.defaultId 104 | } 105 | 106 | func copy(id: String) -> Self { 107 | return Self(id: id, sorted: sorted) 108 | } 109 | 110 | // 指定したactionにひもづく入力をinputsに置き換えて返す 111 | func update(for action: KeyBinding.Action, inputs: [KeyBinding.Input]) -> Self { 112 | let keyBindings = values.filter { $0.action != action } 113 | return KeyBindingSet(id: id, values: keyBindings + [KeyBinding(action, inputs)]) 114 | } 115 | 116 | // MARK: Hashable 117 | static func == (lhs: KeyBindingSet, rhs: KeyBindingSet) -> Bool { 118 | return lhs.id == rhs.id 119 | } 120 | 121 | func hash(into hasher: inout Hasher) { 122 | hasher.combine(id) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /macSKK/LatestReleaseFetcher.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | enum LatestReleaseFetcherError: Error { 7 | case invalidData 8 | } 9 | 10 | @globalActor 11 | actor LatestReleaseFetcher { 12 | public static let shared = LatestReleaseFetcher() 13 | /// 前回取得を開始した時間 14 | private(set) var lastFetchedDate: Date? = nil 15 | private(set) var latestRelease: Release? = nil 16 | private let updateChecker = UpdateChecker() 17 | 18 | func fetch() async throws -> Release { 19 | if let lastFetchedDate { 20 | let interval = -lastFetchedDate.timeIntervalSinceNow 21 | if interval < 60 { 22 | logger.log("前回の更新取得から60秒経っていないため \(Int(interval))秒スリープします") 23 | try await Task.sleep(for: .seconds(interval)) 24 | } 25 | } 26 | 27 | lastFetchedDate = Date() 28 | lastFetchedDate = Date() 29 | let latestRelease = try await updateChecker.fetch() 30 | self.latestRelease = latestRelease 31 | return latestRelease 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /macSKK/MarkedText.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import AppKit 5 | import Foundation 6 | 7 | protocol MarkedTextProtocol { 8 | /** 9 | * 現在の状態をMarkedTextとして出力したときを表す文字列、カーソル位置を返す。 10 | * 11 | * 入力文字列に対する応答例: 12 | * - Shift-A, I 13 | * - [.plain("▽あい")] 14 | * - Shift-A, Shift-I 15 | * - [.markerCompose, .plain("あ\*い")] 16 | * - Shift-A, I, left-key 17 | * - [.markerCompose, .plain("あ"), .cursor, .plain("い")] 18 | * - Shift-A, space 19 | * - [.markerSelect, .emphasize("阿")] 20 | */ 21 | func markedTextElements(inputMode: InputMode) -> [MarkedText.Element] 22 | } 23 | 24 | /// 未確定文字列 (下線で表示される) を表すデータ構造。 25 | struct MarkedText: Equatable { 26 | /// 意味の違う部分文字列ごとの定義。現在のところは下線のスタイルの出し分けにだけ使用する 27 | enum Element: Equatable { 28 | /// ▽ のこと。マーカーという名前はddskkに合わせています。 29 | /// 将来カスタマイズしたときには引数Stringを取るようにするかも。 30 | case markerCompose 31 | /// ▼ のこと。 32 | case markerSelect 33 | /// 細い下線で表示する文字列。未確定文字列の入力中、"[登録中]" などのカーソルを操作できない文字列など 34 | case plain(String) 35 | /// 太い下線で表示する文字列。今選択されている変換候補。 36 | case emphasized(String) 37 | /// カーソル 38 | case cursor 39 | 40 | var attributedString: AttributedString { 41 | switch self { 42 | case .markerCompose: 43 | return Self.plain("▽").attributedString 44 | case .markerSelect: 45 | return Self.emphasized("▼").attributedString 46 | case .plain(let text): 47 | return AttributedString(text, attributes: .init([.underlineStyle: NSUnderlineStyle.single.rawValue])) 48 | case .emphasized(let text): 49 | return AttributedString(text, attributes: .init([.underlineStyle: NSUnderlineStyle.thick.rawValue])) 50 | case .cursor: 51 | return AttributedString("", attributes: .init([.cursor: NSCursor.iBeam])) 52 | } 53 | } 54 | } 55 | let elements: [Element] 56 | 57 | init(_ elements: [Element]) { 58 | self.elements = elements 59 | } 60 | 61 | var attributedString: AttributedString { 62 | if let first = elements.first { 63 | var result = elements.dropFirst().reduce(first.attributedString, { result, current in 64 | return result + current.attributedString 65 | }) 66 | if !elements.contains(where: { $0 == .cursor }) { 67 | result.append(Element.cursor.attributedString) 68 | } 69 | return result 70 | } else { 71 | return AttributedString() 72 | } 73 | } 74 | 75 | func cursorRange() -> NSRange? { 76 | var location: Int = 0 77 | for element in elements { 78 | switch element { 79 | case .markerSelect, .markerCompose: 80 | location += 1 81 | case .plain(let string): 82 | location += string.count 83 | case .emphasized(let string): 84 | location += string.count 85 | case .cursor: 86 | return NSRange(location: location, length: 0) 87 | } 88 | } 89 | return nil 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /macSKK/NSEvent+CoreService.swift: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import AppKit 4 | import CoreServices 5 | import Carbon.HIToolbox.TextInputSources 6 | 7 | extension NSEvent { 8 | /** 9 | * キーイベントについて、修飾キーが押されてないときの文字列を返す。 10 | * 例えば Shift-aなら "A" ではなく "a"、Shift-1なら "!" ではなく "1" を返す。 11 | * 正常に取得できなかった場合は`nil`を返す 12 | * 13 | * > NOTE: macOS 13では ``NSEvent/characters(byApplyingModifiers:)`` がnilを返す問題が発覚したため、 14 | * そのような環境用にCoreServicesの古いAPIで取得している。 15 | */ 16 | var charactersWithoutModifiers: String? { 17 | guard let inputSource = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue(), 18 | let layoutData = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData) else { 19 | return nil 20 | } 21 | 22 | let keyLayoutPtr = unsafeBitCast(CFDataGetBytePtr(unsafeBitCast(layoutData, to: CFData.self)), 23 | to: UnsafePointer.self) 24 | var deadKeyState: UInt32 = 0 25 | let maxStringLength = 4 26 | var actualStringLength = 0 27 | var unicodeString = [UniChar](repeating: 0, count: maxStringLength) 28 | 29 | let status = UCKeyTranslate( 30 | keyLayoutPtr, 31 | keyCode, 32 | UInt16(kUCKeyActionDown), 33 | 0, 34 | UInt32(LMGetKbdType()), 35 | OptionBits(kUCKeyTranslateNoDeadKeysBit), 36 | &deadKeyState, 37 | maxStringLength, 38 | &actualStringLength, 39 | &unicodeString 40 | ) 41 | 42 | guard status == noErr else { 43 | logger.warning("入力されたキーの情報が取得できませんでした: status=\(status)") 44 | return nil 45 | } 46 | 47 | return String(utf16CodeUnits: unicodeString, count: actualStringLength) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /macSKK/Pasteboard.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import AppKit 5 | 6 | /// クリップボード関係の処理。ユニットテスト実行時に実際のNSPasteboardを更新しなくていいようにしてある。 7 | struct Pasteboard { 8 | nonisolated(unsafe) static var stringForTest: String? = nil 9 | 10 | static func getString() -> String? { 11 | if isTest() { 12 | return stringForTest 13 | } else { 14 | let pasteboard = NSPasteboard.general 15 | return pasteboard.string(forType: .string) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /macSKK/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /macSKK/Preview Content/SKK-JISYO.sample.utf-8: -------------------------------------------------------------------------------- 1 | じしょ /辞書/ 2 | -------------------------------------------------------------------------------- /macSKK/Punctuation.swift: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import Foundation 4 | 5 | /** 6 | * カンマとピリオドを入力したときに入力される句読点の設定. 7 | * ローマ字かな変換ルールを上書きすることが可能。 8 | */ 9 | struct Punctuation { 10 | enum Comma: Int, CaseIterable, Identifiable { 11 | typealias ID = Int 12 | var id: ID { rawValue } 13 | /// ローマ字かな変換ルールをそのまま適用する 14 | case `default` = 0 15 | /// "、" を入力する 16 | case ten = 1 17 | /// "," (全角カンマ) を入力する 18 | case comma = 2 19 | 20 | init?(rawValue: Int) { 21 | switch rawValue & 3 { 22 | case 0: 23 | self = .default 24 | case 1: 25 | self = .ten 26 | case 2: 27 | self = .comma 28 | default: 29 | return nil 30 | } 31 | } 32 | 33 | var description: String { 34 | switch self { 35 | case .default: 36 | return String(localized: "Follow Romaji-Kana Rule") 37 | case .ten: 38 | return String(format: String(localized: "EnterKey"), "、") 39 | case .comma: 40 | return String(format: String(localized: "EnterKey"), ",") 41 | } 42 | } 43 | } 44 | 45 | enum Period: Int, CaseIterable, Identifiable { 46 | typealias ID = Int 47 | var id: ID { rawValue } 48 | /// ローマ字かな変換ルールをそのまま適用する 49 | case `default` = 0 50 | /// "。" を入力する 51 | case maru = 256 52 | /// "." (全角ピリオド) を入力する 53 | case period = 512 54 | 55 | init?(rawValue: Int) { 56 | switch rawValue & 768 { 57 | case 0: 58 | self = .default 59 | case 256: 60 | self = .maru 61 | case 512: 62 | self = .period 63 | default: 64 | return nil 65 | } 66 | } 67 | 68 | var description: String { 69 | switch self { 70 | case .default: 71 | return String(localized: "Follow Romaji-Kana Rule") 72 | case .maru: 73 | return String(format: String(localized: "EnterKey"), "。") 74 | case .period: 75 | return String(format: String(localized: "EnterKey"), ".") 76 | } 77 | } 78 | } 79 | 80 | let comma: Comma 81 | let period: Period 82 | 83 | static let `default`: Self = .init(comma: .default, period: .default) 84 | 85 | init(comma: Punctuation.Comma, period: Punctuation.Period) { 86 | self.comma = comma 87 | self.period = period 88 | } 89 | 90 | init?(rawValue: Int) { 91 | guard let comma = Comma(rawValue: rawValue), let period = Period(rawValue: rawValue) else { 92 | return nil 93 | } 94 | self.comma = comma 95 | self.period = period 96 | } 97 | 98 | var rawValue: Int { 99 | comma.rawValue | period.rawValue 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /macSKK/Release+UNNotification.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | import UserNotifications 6 | 7 | extension Release { 8 | /// 新しいバージョンが出たときの通知のID。通知を識別するために使うので将来はバージョンごとに異なるようにするかも。 9 | static let userNotificationIdentifier = "net.mtgto.inputmethod.macSKK.userNotification.newVersion" 10 | static let userNotificationUserInfoKey = "ReleaseVersion" 11 | static let userNotificationUserInfoNameUrl = "url" 12 | 13 | /** 14 | * 新しいバージョンが出たことを通知するための通知リクエストを作成します。 15 | */ 16 | func userNotificationRequest() -> UNNotificationRequest { 17 | let content = UNMutableNotificationContent() 18 | content.title = String(localized: "UNNewVersionTitle", comment: "新しいバージョンがあります") 19 | content.body = String(format: String(localized: "UNNewVersionBody"), version.description) 20 | content.userInfo[Self.userNotificationUserInfoKey] = [Self.userNotificationUserInfoNameUrl: url.absoluteString] 21 | 22 | let request = UNNotificationRequest(identifier: Self.userNotificationIdentifier, content: content, trigger: nil) 23 | return request 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /macSKK/Release.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | struct Release: Sendable, Decodable { 7 | let version: ReleaseVersion 8 | let updated: Date 9 | let url: URL 10 | // HTML形式 11 | let content: String 12 | 13 | enum CodingKeys: String, CodingKey { 14 | case version = "name" 15 | case updated = "published_at" 16 | case url = "html_url" 17 | case content = "body" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /macSKK/ReleaseVersion.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | /// リリースされたバージョン番号。PackageDescription.Versionと同じようにセマンティックバージョニングを採用しています。 7 | struct ReleaseVersion: Comparable, CustomStringConvertible, Sendable, Decodable { 8 | enum ReleaseVersionError: Error { 9 | case invalidFormat 10 | } 11 | 12 | // 将来その下にベータとかアルファの情報を追加するかも 13 | let major: Int 14 | let minor: Int 15 | let patch: Int 16 | 17 | var description: String { 18 | return "\(major).\(minor).\(patch)" 19 | } 20 | 21 | // Comparable 22 | static func < (lhs: Self, rhs: Self) -> Bool { 23 | if lhs.major != rhs.major { 24 | return lhs.major < rhs.major 25 | } else if lhs.minor != rhs.minor { 26 | return lhs.minor < rhs.minor 27 | } else { 28 | return lhs.patch < rhs.patch 29 | } 30 | } 31 | 32 | init(major: Int, minor: Int, patch: Int) { 33 | self.major = major 34 | self.minor = minor 35 | self.patch = patch 36 | } 37 | 38 | init(string: String) throws { 39 | guard let match = string.wholeMatch(of: /([0-9]+)\.([0-9]+)\.([0-9]+)/), 40 | let major = Int(match.1), 41 | let minor = Int(match.2), 42 | let patch = Int(match.3) else { 43 | throw ReleaseVersionError.invalidFormat 44 | } 45 | self.major = major 46 | self.minor = minor 47 | self.patch = patch 48 | } 49 | 50 | init(from decoder: any Decoder) throws { 51 | let string = try decoder.singleValueContainer().decode(String.self) 52 | try self.init(string: string) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /macSKK/SKKServDict.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | /** 7 | * skkservを辞書として使う辞書定義 8 | * 9 | * 複数のskkservを想定してSKKServDict(サーバー数)とSKKServService(1つ)と分けているけど、 10 | * 当面はサーバー数を1に固定してSKKServDictにXPCとの通信処理をもってきたほうがシンプルかも? 11 | */ 12 | struct SKKServDict { 13 | private let destination: SKKServDestination 14 | private let service: any SKKServServiceProtocol 15 | 16 | init(destination: SKKServDestination, service: any SKKServServiceProtocol = SKKServService()) { 17 | self.destination = destination 18 | self.service = service 19 | } 20 | 21 | /** 22 | * skkservに変換候補の問い合わせを行い変換候補を返す。 23 | * 24 | * TCP接続が切れたり接続タイムアウトや応答のタイムアウトした場合はログだけ出力して空配列を返す。 25 | */ 26 | func refer(_ yomi: String, option: DictReferringOption?) -> [Word] { 27 | do { 28 | let result = try service.refer(yomi: yomi, destination: destination, timeout: 1.0) 29 | // 変換候補が見つかった場合は "1/変換/返還/" のように 1が先頭でスラッシュで区切られた文字列 30 | // 見つからなかった場合は "4へんかん" のように4が先頭の文字列 31 | guard result.hasPrefix("1/") else { 32 | logger.debug("skkservから変換候補が見つからなかったレスポンスが返りました") 33 | return [] 34 | } 35 | // Entry.parseWordsは先頭のスラッシュがない形を受け取る 36 | guard let candidates = Entry.parseWords(result.dropFirst(2), dictId: "skkserv") else { 37 | logger.error("skkservの返した変換候補を正常にパースできませんでした") 38 | return [] 39 | } 40 | return candidates 41 | } catch { 42 | if let error = error as? SKKServClientError { 43 | switch error { 44 | case .connectionRefused: 45 | logger.log("skkservが応答しません") 46 | case .connectionTimeout: 47 | logger.log("skkservとの接続がタイムアウトしました") 48 | case .invalidResponse: 49 | logger.warning("skkservから想定しない応答が返りました") 50 | case .timeout: 51 | logger.log("skkservから応答が一定時間返りませんでした") 52 | default: 53 | logger.error("skkservから不明なエラーが返りました") 54 | } 55 | } else { 56 | logger.error("skkserv辞書の検索でエラーが発生しました: \(error, privacy: .public)") 57 | } 58 | return [] 59 | } 60 | } 61 | 62 | func disconnect() { 63 | do { 64 | try service.disconnect() 65 | } catch { 66 | logger.error("skkservとの通信切断でエラーが発生しました: \(error, privacy: .public)") 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /macSKK/Settings/DictionaryView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | struct DictionaryView: View { 7 | @Binding var dictSetting: DictSetting? 8 | let filename: String 9 | @State var encoding: String.Encoding 10 | 11 | var body: some View { 12 | VStack { 13 | Form { 14 | Section("Dictionary Setting") { 15 | LabeledContent("Filename") { 16 | Text(filename) 17 | } 18 | Picker("Encoding", selection: $encoding) { 19 | ForEach(AllowedEncoding.allCases, id: \.encoding) { allowedEncoding in 20 | Text(allowedEncoding.description).tag(allowedEncoding.encoding) 21 | } 22 | } 23 | .pickerStyle(.radioGroup) 24 | .disabled( 25 | // SKK-JISYO.Lは特定の文字コードでmacSKKに同梱される。(Makefileでビルド時にダウンロードしている) 26 | // ユーザーがこれをnkfなどでUTF-8に変更したとしても、macSKKのバージョンをアップデートするたびに 27 | // 上書きされて文字コードが異なって読み込みエラーとなる可能性がある。 28 | // 従ってファイル名が`SKK-JISYO.L`な場合は文字コードの変更を禁止する。 29 | // (文字コードを変更したい場合は、SKK-JISYO.Lをdisableにして別名で辞書ディレクトリーに設置するべきと思われる) 30 | filename == "SKK-JISYO.L" || dictSetting?.type == .json 31 | ) 32 | if filename == "SKK-JISYO.L" { 33 | HStack { 34 | Image(systemName: "exclamationmark.triangle") 35 | Text("Unable to change encoding SKK-JISYO.L") 36 | } 37 | } 38 | } 39 | } 40 | .formStyle(.grouped) 41 | Divider() 42 | HStack { 43 | Spacer() 44 | Button { 45 | if let dictSetting { 46 | if case .traditional = dictSetting.type { 47 | dictSetting.type = .traditional(encoding) 48 | } 49 | } 50 | // このビューを閉じる 51 | dictSetting = nil 52 | } label: { 53 | Text("Done") 54 | .padding([.leading, .trailing]) 55 | } 56 | .keyboardShortcut(.defaultAction) 57 | .padding([.trailing, .bottom, .top]) 58 | } 59 | Spacer() 60 | } 61 | .frame(width: 480, height: 270) 62 | } 63 | } 64 | 65 | struct DictionaryView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | DictionaryView( 68 | dictSetting: .constant(nil), 69 | filename: "SKK-JISYO.sample.utf-8", 70 | encoding: .utf8 71 | ).previewDisplayName("SKK-JISYO.sample.utf-8") 72 | DictionaryView( 73 | dictSetting: .constant(nil), 74 | filename: "SKK-JISYO.L", 75 | encoding: .japaneseEUC 76 | ).previewDisplayName("SKK-JISYO.L") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /macSKK/Settings/DirectModeView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | struct DirectModeView: View { 7 | @StateObject var settingsViewModel: SettingsViewModel 8 | 9 | var body: some View { 10 | let applications = settingsViewModel.directModeApplications 11 | VStack { 12 | Form { 13 | if applications.isEmpty { 14 | Text("Unregistered") 15 | } else { 16 | Section { 17 | List(applications) { application in 18 | HStack { 19 | if let icon = application.icon { 20 | Image(nsImage: icon) 21 | .frame(width: 32, height: 32) 22 | } else { 23 | Image(systemName: "questionmark.square") 24 | .font(.system(size: 32)) 25 | .fontWeight(.light) 26 | } 27 | Text(application.displayName ?? application.bundleIdentifier) 28 | Spacer() 29 | Button { 30 | if let index = applications.firstIndex(of: application) { 31 | logger.log("Bundle Identifier \"\(applications[index].bundleIdentifier, privacy: .public)\" の直接入力が解除されました。") 32 | settingsViewModel.directModeApplications.remove(at: index) 33 | } 34 | } label: { 35 | Text("Delete") 36 | } 37 | } 38 | .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) 39 | .onAppear { 40 | if application.icon == nil || application.displayName == nil { 41 | let workspace = NSWorkspace.shared 42 | if let index = applications.firstIndex(of: application), 43 | let appUrl = workspace.urlForApplication(withBundleIdentifier: application.bundleIdentifier) { 44 | settingsViewModel.updateDirectModeApplication(index: index, displayName: FileManager.default.displayName(atPath: appUrl.path(percentEncoded: false)), icon: workspace.icon(forFile: appUrl.path(percentEncoded: false))) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | .formStyle(.grouped) 53 | Text("SettingsNoteDirectMode") 54 | .font(.subheadline) 55 | .padding([.bottom, .leading, .trailing]) 56 | Spacer() 57 | } 58 | } 59 | } 60 | 61 | struct DirectModeView_Previews: PreviewProvider { 62 | static var previews: some View { 63 | DirectModeView(settingsViewModel: try! SettingsViewModel(directModeApplications: [ 64 | DirectModeApplication(bundleIdentifier: "net.mtgto.inputmethod.macSKK", icon: nil, displayName: nil), 65 | ])) 66 | DirectModeView(settingsViewModel: try! SettingsViewModel(directModeApplications: [])) 67 | .previewDisplayName("空のとき") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /macSKK/Settings/GeneralView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | struct GeneralView: View { 7 | @StateObject var settingsViewModel: SettingsViewModel 8 | 9 | var body: some View { 10 | VStack { 11 | Form { 12 | Toggle(isOn: $settingsViewModel.enterNewLine, label: { 13 | Text("Enter Key confirms a candidate and sends a newline") 14 | }) 15 | Picker("Keyboard Layout", selection: $settingsViewModel.selectedInputSourceId) { 16 | ForEach(settingsViewModel.inputSources) { inputSource in 17 | Text(inputSource.localizedName) 18 | } 19 | } 20 | Picker("Direction of candidate list", selection: $settingsViewModel.candidateListDirection) { 21 | ForEach(CandidateListDirection.allCases, id: \.id) { listDirection in 22 | Text(listDirection.description).tag(listDirection) 23 | } 24 | } 25 | Toggle(isOn: $settingsViewModel.showAnnotation, label: { 26 | Text("Show Annotation") 27 | }) 28 | Picker("System Dictionary for annotation", selection: $settingsViewModel.systemDict) { 29 | Text("SystemDictDaijirin").tag(SystemDict.Kind.daijirin) 30 | Text("SystemDictWisdom").tag(SystemDict.Kind.wisdom) 31 | }.disabled(!settingsViewModel.showAnnotation) 32 | Picker("Keys of selecting candidates", selection: $settingsViewModel.selectCandidateKeys) { 33 | Text("123456789").tag("123456789") 34 | Text("ASDFGHJKL").tag("ASDFGHJKL") 35 | Text("AOEUIDHTN").tag("AOEUIDHTN") 36 | } 37 | Picker("Behavior of Comma", selection: $settingsViewModel.comma) { 38 | ForEach(Punctuation.Comma.allCases, id: \.id) { comma in 39 | Text(comma.description).tag(comma) 40 | } 41 | } 42 | Picker("Behavior of Period", selection: $settingsViewModel.period) { 43 | ForEach(Punctuation.Period.allCases, id: \.id) { period in 44 | Text(period.description).tag(period) 45 | } 46 | } 47 | Toggle(isOn: $settingsViewModel.showInputIconModal, label: { 48 | Text("Show Input Mode Modal") 49 | }) 50 | Section { 51 | Picker("Number of inline candidates", selection: $settingsViewModel.inlineCandidateCount) { 52 | ForEach(0..<10) { count in 53 | Text("\(count)") 54 | } 55 | } 56 | Picker("Backspace in selecting candidates", selection: $settingsViewModel.selectingBackspace) { 57 | ForEach(SelectingBackspace.allCases, id: \.id) { selectingBackspace in 58 | Text(selectingBackspace.description).tag(selectingBackspace) 59 | } 60 | } 61 | } 62 | Section { 63 | Picker("Candidates font size", selection: $settingsViewModel.candidatesFontSize) { 64 | ForEach(6..<31) { count in 65 | Text("\(count)").tag(count) 66 | } 67 | } 68 | Picker("Annotation font size", selection: $settingsViewModel.annotationFontSize) { 69 | ForEach(6..<31) { count in 70 | Text("\(count)").tag(count) 71 | } 72 | } 73 | } 74 | Section { 75 | Toggle(isOn: $settingsViewModel.showCompletion, label: { 76 | Text("Show Completion") 77 | }) 78 | Toggle(isOn: $settingsViewModel.findCompletionFromAllDicts, label: { 79 | Text("Find completion from all dictionaries") 80 | }).disabled(!settingsViewModel.showCompletion) 81 | Toggle(isOn: $settingsViewModel.ignoreUserDictInPrivateMode, label: { 82 | Text("Ignore User Dict in Private Mode") 83 | }) 84 | } 85 | } 86 | .formStyle(.grouped) 87 | }.onAppear { 88 | settingsViewModel.loadInputSources() 89 | } 90 | } 91 | } 92 | 93 | #Preview { 94 | GeneralView(settingsViewModel: try! SettingsViewModel(inputSources: [InputSource(id: "com.example.qwerty", localizedName: "Qwerty")])) 95 | } 96 | -------------------------------------------------------------------------------- /macSKK/Settings/KeyBinding/KeyBindingSetView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | // KeyBindingSetの命名画面 (複製時・リネーム時) 7 | struct KeyBindingSetView: View { 8 | enum Mode: Identifiable { 9 | var id: Int { 10 | switch self { 11 | case .duplicate: 12 | return 0 13 | case .rename: 14 | return 1 15 | } 16 | } 17 | 18 | case duplicate(KeyBindingSet) 19 | case rename(KeyBindingSet) 20 | } 21 | 22 | @StateObject var settingsViewModel: SettingsViewModel 23 | // nilのときはこのビューが表示されてない状態 24 | @Binding var mode: Mode? 25 | @State var id: String 26 | 27 | var body: some View { 28 | VStack { 29 | Form { 30 | TextField("Name of KeyBinding Set", text: $id) 31 | } 32 | .formStyle(.grouped) 33 | Divider() 34 | HStack { 35 | Spacer() 36 | Button("Cancel", role: .cancel) { 37 | mode = nil 38 | } 39 | Button { 40 | if case .duplicate(let keyBindingSet) = mode { 41 | settingsViewModel.keyBindingSets.append(keyBindingSet.copy(id: id)) 42 | settingsViewModel.selectedKeyBindingSet = settingsViewModel.keyBindingSets.last! 43 | logger.log("キーバインドのセット \(keyBindingSet.id, privacy: .public) から \(id, privacy: .public) を複製しました") 44 | } else if case .rename(let keyBindingSet) = mode { 45 | if let index = settingsViewModel.keyBindingSets.firstIndex(of: keyBindingSet) { 46 | settingsViewModel.keyBindingSets[index] = keyBindingSet.copy(id: id) 47 | settingsViewModel.selectedKeyBindingSet = settingsViewModel.keyBindingSets[index] 48 | logger.log("キーバインドのセット \(keyBindingSet.id, privacy: .public) の名前を \(id, privacy: .public) に変更しました") 49 | } 50 | } 51 | mode = nil 52 | } label: { 53 | Text("Done") 54 | .padding([.leading, .trailing]) 55 | } 56 | .keyboardShortcut(.defaultAction) 57 | .padding([.trailing, .bottom, .top]) 58 | .disabled(!canSave) 59 | } 60 | Spacer() 61 | } 62 | .frame(width: 480, height: 270) 63 | } 64 | 65 | var canSave: Bool { 66 | if id.isEmpty { 67 | return false 68 | } 69 | guard let mode else { return false } 70 | let count = settingsViewModel.keyBindingSets.reduce(0, { (sum, acc) in acc.id == id ? sum + 1 : sum }) 71 | switch mode { 72 | case .duplicate: 73 | return count == 0 74 | case .rename: 75 | return count <= 1 76 | } 77 | } 78 | } 79 | 80 | #Preview { 81 | KeyBindingSetView( 82 | settingsViewModel: try! SettingsViewModel(selectedKeyBindingSet: nil), 83 | mode: .constant(.duplicate(KeyBindingSet.defaultKeyBindingSet)), 84 | id: KeyBindingSet.defaultKeyBindingSet.id) 85 | } 86 | -------------------------------------------------------------------------------- /macSKK/Settings/KeyBinding/KeyBindingView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | struct KeyBindingView: View { 7 | @StateObject var settingsViewModel: SettingsViewModel 8 | @State var editingKeyBindingSetMode: KeyBindingSetView.Mode? = nil 9 | @State var isShowingConfirmDeleteAlert: Bool = false 10 | @State var isEditingKeyBindingInputs: Bool = false 11 | @State var editingKeyBindingAction: KeyBinding.Action = .abbrev 12 | @State var editingKeyBindingInputs: [KeyBindingInput] = [] 13 | @State var selectingKeyBindingAction: KeyBinding.Action? = nil 14 | 15 | var body: some View { 16 | HStack { 17 | Picker("Settings", selection: $settingsViewModel.selectedKeyBindingSet) { 18 | ForEach(settingsViewModel.keyBindingSets) { keyBindingSet in 19 | Text(keyBindingSet.id) 20 | .tag(keyBindingSet) 21 | } 22 | } 23 | Menu { 24 | Button { 25 | editingKeyBindingSetMode = .rename(settingsViewModel.selectedKeyBindingSet) 26 | } label: { 27 | Text("Rename") 28 | }.disabled(!settingsViewModel.selectedKeyBindingSet.canEdit) 29 | Button { 30 | editingKeyBindingSetMode = .duplicate(settingsViewModel.selectedKeyBindingSet) 31 | } label: { 32 | Text("Duplicate") 33 | } 34 | Button { 35 | // アラート出してから消す 36 | isShowingConfirmDeleteAlert = true 37 | } label: { 38 | Text("Delete") 39 | }.disabled(!settingsViewModel.selectedKeyBindingSet.canDelete) 40 | } label: { 41 | Image(systemName: "ellipsis") 42 | } 43 | .buttonStyle(.borderless) 44 | } 45 | .padding([.top, .leading, .trailing]) 46 | .sheet(item: $editingKeyBindingSetMode) { mode in 47 | let copyId = String(format: String(localized: "DuplicatedKeyBindingName"), settingsViewModel.selectedKeyBindingSet.id) 48 | KeyBindingSetView(settingsViewModel: settingsViewModel, mode: $editingKeyBindingSetMode, id: copyId) 49 | } 50 | .confirmationDialog("Delete", isPresented: $isShowingConfirmDeleteAlert) { 51 | Button("Cancel", role: .cancel) { 52 | isShowingConfirmDeleteAlert = false 53 | } 54 | Button("Delete", role: .destructive) { 55 | let index = settingsViewModel.keyBindingSets.firstIndex { keyBindingSet in 56 | keyBindingSet.id == settingsViewModel.selectedKeyBindingSet.id 57 | } 58 | if let index { 59 | logger.log("キーバインドのセット \(settingsViewModel.selectedKeyBindingSet.id, privacy: .public) を削除しました") 60 | settingsViewModel.selectedKeyBindingSet = settingsViewModel.keyBindingSets[index - 1] 61 | settingsViewModel.keyBindingSets.remove(at: index) 62 | } 63 | isShowingConfirmDeleteAlert = false 64 | } 65 | } message: { 66 | Text("Are you sure you want to delete \(settingsViewModel.selectedKeyBindingSet.id)?") 67 | } 68 | Form { 69 | Table(settingsViewModel.selectedKeyBindingSet.values, selection: $selectingKeyBindingAction) { 70 | TableColumn("Action") { keyBinding in 71 | Text(keyBinding.action.localizedAction).fontWeight(keyBinding.isDefault ? nil : .bold) 72 | } 73 | TableColumn("Key", value: \.localizedInputs) 74 | } 75 | .contextMenu(forSelectionType: KeyBinding.ID.self) { keyBindingActions in 76 | Button("Edit") { 77 | if let action = keyBindingActions.first, let keyBinding = settingsViewModel.selectedKeyBindingSet.values.first(where: { $0.action == action }) { 78 | editingKeyBindingAction = action 79 | editingKeyBindingInputs = keyBinding.inputs.map { KeyBindingInput(input: $0) } 80 | isEditingKeyBindingInputs = true 81 | } 82 | } 83 | .disabled(!settingsViewModel.selectedKeyBindingSet.canEdit) 84 | Button("Reset") { 85 | if let action = keyBindingActions.first { 86 | settingsViewModel.resetKeyBindingInputs(action: action) 87 | } 88 | } 89 | } primaryAction: { keyBindingActions in 90 | if settingsViewModel.selectedKeyBindingSet.canEdit { 91 | if let action = keyBindingActions.first, let keyBinding = settingsViewModel.selectedKeyBindingSet.values.first(where: { $0.action == action }) { 92 | editingKeyBindingAction = action 93 | editingKeyBindingInputs = keyBinding.inputs.map { KeyBindingInput(input: $0) } 94 | isEditingKeyBindingInputs = true 95 | } 96 | } 97 | } 98 | .sheet(isPresented: $isEditingKeyBindingInputs) { 99 | KeyBindingInputsView(settingsViewModel: settingsViewModel, 100 | action: $editingKeyBindingAction, 101 | inputs: $editingKeyBindingInputs) 102 | } 103 | } 104 | .formStyle(.grouped) 105 | } 106 | } 107 | 108 | #Preview { 109 | KeyBindingView(settingsViewModel: try! SettingsViewModel(keyBindings: KeyBinding.defaultKeyBindingSettings)) 110 | } 111 | -------------------------------------------------------------------------------- /macSKK/Settings/KeyEventView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #if DEBUG 5 | 6 | import SwiftUI 7 | 8 | struct KeyEventView: View { 9 | @State private var text: String = "" 10 | @State private var eventMonitor: Any! 11 | @State private var characters: String = "" 12 | @State private var charactersIgnoringModifiers: String = "" 13 | @State private var keyCode: String = "" 14 | @State private var modifiers: String = "" 15 | @State private var keyBinding: KeyBinding.Action? = nil 16 | 17 | var body: some View { 18 | VStack(alignment: .leading) { 19 | TextField("", text: $text) 20 | .onAppear { 21 | eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { event in 22 | characters = event.characters ?? "" 23 | // event.charactersIgnoringModifiersは IMKInputControllerへの入力とは違って 24 | // Shiftを押しながらだと変わる記号はそのままになっているという違いがある。 25 | // そのためIMKInputControllerに渡されるのに近い「修飾キーがないときの入力」を取得する。 26 | if #available(macOS 14, *) { 27 | charactersIgnoringModifiers = event.characters(byApplyingModifiers: []) ?? "" 28 | } else { 29 | charactersIgnoringModifiers = event.charactersWithoutModifiers ?? "" 30 | } 31 | keyCode = event.keyCode.description 32 | var modifiers: [String] = [] 33 | if event.modifierFlags.contains(.capsLock) { 34 | modifiers.append("CapsLock") 35 | } 36 | if event.modifierFlags.contains(.shift) { 37 | modifiers.append("Shift") 38 | } 39 | if event.modifierFlags.contains(.control) { 40 | modifiers.append("Control") 41 | } 42 | if event.modifierFlags.contains(.option) { 43 | modifiers.append("Option") 44 | } 45 | if event.modifierFlags.contains(.command) { 46 | modifiers.append("Command") 47 | } 48 | if event.modifierFlags.contains(.numericPad) { 49 | modifiers.append("NumericPad") 50 | } 51 | if event.modifierFlags.contains(.help) { 52 | modifiers.append("Help") 53 | } 54 | if event.modifierFlags.contains(.function) { 55 | modifiers.append("Fn") 56 | } 57 | self.modifiers = modifiers.joined(separator: ", ") 58 | // toggleKanaとtoggleAndFixKanaが判別できないけどデバッグ機能なのでよしとする 59 | self.keyBinding = Global.keyBinding.action(event: event, inputMethodState: .normal) 60 | 61 | return event 62 | } 63 | } 64 | .onDisappear { 65 | NSEvent.removeMonitor(eventMonitor!) 66 | } 67 | Form { 68 | Section { 69 | TextField("KeyBinding", text: .constant(keyBinding?.stringValue ?? "")) 70 | } 71 | Section { 72 | TextField("KeyCode", text: .constant(keyCode)) 73 | } 74 | Section { 75 | TextField("Characters", text: .constant(characters)) 76 | } 77 | Section { 78 | TextField("CharactersIgnoringModifiers", text: .constant(charactersIgnoringModifiers)) 79 | } 80 | Section { 81 | TextField("Modifiers", text: .constant(modifiers)) 82 | } 83 | } 84 | Spacer() 85 | } 86 | .padding() 87 | } 88 | } 89 | 90 | struct KeyEventView_Previews: PreviewProvider { 91 | static var previews: some View { 92 | KeyEventView() 93 | } 94 | } 95 | 96 | #endif 97 | -------------------------------------------------------------------------------- /macSKK/Settings/LogView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import OSLog 5 | import SwiftUI 6 | 7 | struct LogView: View { 8 | @State var log: String 9 | @State private var loading: Bool = false 10 | 11 | var body: some View { 12 | Form { 13 | TextEditor(text: .constant(log)) 14 | Spacer() 15 | HStack { 16 | if loading { 17 | ProgressView().controlSize(.small) 18 | } 19 | Spacer() 20 | Button { 21 | let pasteboard = NSPasteboard.general 22 | pasteboard.clearContents() 23 | pasteboard.setString(log, forType: .string) 24 | } label: { 25 | Text("Copy") 26 | } 27 | .disabled(loading) 28 | } 29 | 30 | } 31 | .padding() 32 | .task { 33 | loading = true 34 | do { 35 | self.log = try await load() 36 | } catch { 37 | self.log = "アプリケーションログが取得できません: \(error)" 38 | logger.error("アプリケーションログが取得できません: \(error)") 39 | } 40 | loading = false 41 | } 42 | } 43 | 44 | nonisolated private func load() async throws -> String { 45 | func levelDescription(level: OSLogEntryLog.Level) -> String { 46 | switch level { 47 | case .undefined: 48 | return "undefined" 49 | case .debug: 50 | return "debug" 51 | case .info: 52 | return "info" 53 | case .notice: 54 | return "notice" 55 | case .error: 56 | return "error" 57 | case .fault: 58 | return "fault" 59 | @unknown default: 60 | logger.error("未知のログレベル \(level.rawValue) が使用されました") 61 | return "unknown" 62 | } 63 | } 64 | let logStore = try OSLogStore(scope: .currentProcessIdentifier) 65 | let predicate = NSPredicate(format: "subsystem == %@", Bundle.main.bundleIdentifier!) 66 | let logs = try logStore.getEntries(matching: predicate).compactMap { $0 as? OSLogEntryLog } 67 | let format = Date.ISO8601FormatStyle.iso8601Date(timeZone: TimeZone.current) 68 | .dateTimeSeparator(.space) 69 | .time(includingFractionalSeconds: true) 70 | return logs.map { entry in 71 | [ 72 | "[\(entry.date.formatted(format))]", 73 | "[\(levelDescription(level: entry.level))]", 74 | "\(entry.composedMessage)", 75 | ].joined(separator: " ") 76 | }.joined(separator: "\n") 77 | } 78 | } 79 | 80 | #Preview { 81 | LogView(log: ["[2024-01-01 12:34:56.789] [info] ほげほげがほげほげしました", "[2024-01-01 12:34:56.789] [info] ふがふががふがふがしました"].joined(separator: "\n")) 82 | } 83 | -------------------------------------------------------------------------------- /macSKK/Settings/SoftwareUpdateView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | struct SoftwareUpdateView: View { 7 | @Environment(\.openURL) private var openURL 8 | @StateObject var settingsViewModel: SettingsViewModel 9 | 10 | var body: some View { 11 | VStack { 12 | Form { 13 | Section { 14 | LabeledContent("Current Version:") { 15 | Text(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String) 16 | if let latestRelease = settingsViewModel.latestRelease { 17 | Text("Latest Version: \(latestRelease.version.description)") 18 | .padding(.leading) 19 | } 20 | } 21 | if let latestRelease = settingsViewModel.latestRelease, 22 | let markdown = try? AttributedString(markdown: latestRelease.content, 23 | options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)) { 24 | Text(markdown).textSelection(.enabled) 25 | } 26 | HStack { 27 | Spacer() 28 | Button("Check For Update") { 29 | fetchReleases() 30 | } 31 | .disabled(settingsViewModel.fetchingRelease) 32 | if settingsViewModel.fetchingRelease { 33 | ProgressView() 34 | .progressViewStyle(.circular) 35 | .controlSize(.small) 36 | .padding(.leading) 37 | } 38 | } 39 | } footer: { 40 | Button("Open Release Page") { 41 | if let url = URL(string: "https://github.com/mtgto/macSKK/releases") { 42 | openURL(url) 43 | } 44 | } 45 | } 46 | } 47 | .formStyle(.grouped) 48 | .padding() 49 | Spacer() 50 | } 51 | } 52 | 53 | private func fetchReleases() { 54 | Task { 55 | try await _ = settingsViewModel.fetchLatestRelease() 56 | } 57 | } 58 | } 59 | 60 | struct SoftwareUpdateView_Previews: PreviewProvider { 61 | static var previews: some View { 62 | let viewModel = try! SettingsViewModel(dictSettings: []) 63 | viewModel.latestRelease = Release(version: ReleaseVersion(major: 1, minor: 0, patch: 0), 64 | updated: Date(), 65 | url: URL(string: "https://example.com")!, 66 | content: "- すばらしい**機能**を実装しました (#9999)\n- バグを修正しました (#10000)") 67 | return SoftwareUpdateView(settingsViewModel: viewModel) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /macSKK/Settings/SystemDictView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #if DEBUG 5 | 6 | import SwiftUI 7 | 8 | /// システム辞書で引いてみるデバッグ機能。 9 | struct SystemDictView: View { 10 | @State private var word: String = "" 11 | @State private var selectedDict: SystemDict.Kind = SystemDict.Kind.daijirin 12 | @State private var displayText: String = "" 13 | 14 | var body: some View { 15 | Form { 16 | Section(header: Text("見出し")) { 17 | TextField("", text: $word) 18 | .onChange(of: word) { newValue in 19 | if newValue.isEmpty { 20 | displayText = "" 21 | } else if let found = SystemDict.lookup(newValue, for: selectedDict) { 22 | displayText = found 23 | } else { 24 | displayText = "見つかりませんでした" 25 | } 26 | } 27 | } 28 | Section(header: Text("辞書")) { 29 | Picker("", selection: $selectedDict) { 30 | Text("スーパー大辞林").tag(SystemDict.Kind.daijirin) 31 | Text("ウィズダム英和・和英").tag(SystemDict.Kind.wisdom) 32 | } 33 | } 34 | Section(header: Text("結果")) { 35 | TextEditor(text: .constant(displayText)) 36 | } 37 | } 38 | .padding() 39 | } 40 | } 41 | 42 | struct SystemDictView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | SystemDictView() 45 | } 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /macSKK/Settings/UserDefaultsKeys.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | /** 7 | * UserDefaultsのキー。camelCaseでの命名を採用しています。 8 | * キーを追加するときは macSKKApp#setupUserDefaults で初期設定を設定するようにしてください。 9 | */ 10 | struct UserDefaultsKeys { 11 | static let dictionaries = "dictionaries" 12 | static let directModeBundleIdentifiers = "directModeBundleIdentifiers" 13 | // 選択中のinputSourceID 14 | static let selectedInputSource = "selectedInputSource" 15 | static let showAnnotation = "showAnnotation" 16 | static let inlineCandidateCount = "inlineCandidateCount" 17 | static let workarounds = "workarounds" 18 | static let candidatesFontSize = "candidatesFontSize" 19 | static let annotationFontSize = "annotationFontSize" 20 | // SKK辞書サーバーへの接続設定 21 | static let skkservClient = "skkserv" 22 | // 選択候補パネルから決定するショートカットキー。 23 | // 初期値は "123456789"。 24 | static let selectCandidateKeys = "selectCandidateKeys" 25 | static let findCompletionFromAllDicts = "findCompletionFromAllDicts" 26 | // 選択中のキーバインド設定ID 27 | static let selectedKeyBindingSetId = "selectedKeyBindingSetId" 28 | // キーバインド設定の配列 29 | static let keyBindingSets = "keyBindingSets" 30 | // Enterキーで変換候補の確定 + 改行も行う 31 | static let enterNewLine = "enterNewLine" 32 | // 補完を表示するか 33 | static let showCompletion = "showCompletion" 34 | // 注釈に使用するシステム辞書のID。SystemDict.Kindで定義。 35 | static let systemDict = "systemDict" 36 | // 変換候補選択中のバックスペースの挙動 37 | static let selectingBackspace = "selectingBackspace" 38 | // カンマ、ピリオド入力時の句読点 39 | static let punctuation = "punctuation" 40 | static let privateMode = "privateMode" 41 | // プライベートモード時に変換候補にユーザー辞書を無視するかどうか 42 | static let ignoreUserDictInPrivateMode = "ignoreUserDictInPrivateMode" 43 | // 入力モードのモーダルを表示するかどうか 44 | static let showInputModePanel = "showInputModePanel" 45 | // 候補リストの表示方向 46 | static let candidateListDirection = "candidateListDirection" 47 | } 48 | -------------------------------------------------------------------------------- /macSKK/Settings/WorkaroundApplicationView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | struct WorkaroundApplicationView: View { 7 | @StateObject var settingsViewModel: SettingsViewModel 8 | @Binding var bundleIdentifier: String 9 | @Binding var insertBlankString: Bool 10 | @Binding var isShowingSheet: Bool 11 | 12 | var body: some View { 13 | Form { 14 | Section { 15 | TextField("Bundle Identifier", text: $bundleIdentifier) 16 | Toggle("Insert Blank String", isOn: $insertBlankString) 17 | .toggleStyle(.switch) 18 | } header: { 19 | Text("SettingsHeaderWorkaroundApplication") 20 | } footer: { 21 | HStack { 22 | Spacer() 23 | Button(role: .cancel) { 24 | isShowingSheet = false 25 | } label: { 26 | Text("Cancel") 27 | } 28 | .keyboardShortcut(.cancelAction) 29 | Button { 30 | settingsViewModel.workaroundApplications.append( 31 | WorkaroundApplication(bundleIdentifier: bundleIdentifier, insertBlankString: insertBlankString)) 32 | isShowingSheet = false 33 | } label: { 34 | Text("Add") 35 | } 36 | .keyboardShortcut(.defaultAction) 37 | .disabled(bundleIdentifier.isEmpty) 38 | } 39 | } 40 | } 41 | .formStyle(.grouped) 42 | .frame(width: 480) 43 | } 44 | } 45 | 46 | #Preview { 47 | WorkaroundApplicationView(settingsViewModel: try! SettingsViewModel(), 48 | bundleIdentifier: .constant("net.mtgto.inputmethod.macSKK"), 49 | insertBlankString: .constant(true), 50 | isShowingSheet: .constant(true)) 51 | } 52 | -------------------------------------------------------------------------------- /macSKK/Settings/WorkaroundView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | struct WorkaroundView: View { 7 | @StateObject var settingsViewModel: SettingsViewModel 8 | @State var isShowingSheet: Bool = false 9 | // 編集中のアプリケーション設定 10 | @State var bundleIdentifier: String = "" 11 | @State var insertBlankString: Bool = true 12 | 13 | var body: some View { 14 | let applications = settingsViewModel.workaroundApplications 15 | VStack { 16 | Form { 17 | if applications.isEmpty { 18 | Text("Unregistered") 19 | } else { 20 | Section { 21 | List(applications) { application in 22 | HStack { 23 | if let icon = application.icon { 24 | Image(nsImage: icon) 25 | .resizable() 26 | .frame(width: 32, height: 32) 27 | } else { 28 | Image(systemName: "questionmark.square") 29 | .font(.system(size: 32)) 30 | .fontWeight(.light) 31 | .frame(width: 32, height: 32) 32 | } 33 | VStack(alignment: .leading) { 34 | Text(application.displayName ?? application.bundleIdentifier) 35 | .font(.body) 36 | Group { 37 | Text("Insert Blank String") + Text(": ") + Text(application.insertBlankString ? "Enabled" : "Disabled") 38 | }.font(.footnote) 39 | } 40 | Spacer() 41 | Button { 42 | if let index = applications.firstIndex(of: application) { 43 | logger.log("Bundle Identifier \"\(applications[index].bundleIdentifier, privacy: .public)\" の互換性設定が解除されました。") 44 | settingsViewModel.workaroundApplications.remove(at: index) 45 | } 46 | } label: { 47 | Text("Delete") 48 | } 49 | } 50 | .padding(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)) 51 | .onAppear { 52 | if application.icon == nil || application.displayName == nil { 53 | let workspace = NSWorkspace.shared 54 | if let index = applications.firstIndex(of: application), 55 | let appUrl = workspace.urlForApplication(withBundleIdentifier: application.bundleIdentifier) { 56 | settingsViewModel.updateWorkaroundApplication(index: index, displayName: FileManager.default.displayName(atPath: appUrl.path(percentEncoded: false)), icon: workspace.icon(forFile: appUrl.path(percentEncoded: false))) 57 | } 58 | } 59 | } 60 | } 61 | } footer: { 62 | Button { 63 | bundleIdentifier = "" 64 | insertBlankString = true 65 | isShowingSheet = true 66 | } label: { 67 | Text("Add…") 68 | } 69 | } 70 | } 71 | } 72 | .formStyle(.grouped) 73 | Text("SettingsNoteWorkaround") 74 | .font(.subheadline) 75 | .padding([.bottom, .leading, .trailing]) 76 | Spacer() 77 | }.sheet(isPresented: $isShowingSheet) { 78 | WorkaroundApplicationView(settingsViewModel: settingsViewModel, 79 | bundleIdentifier: $bundleIdentifier, 80 | insertBlankString: $insertBlankString, 81 | isShowingSheet: $isShowingSheet) 82 | } 83 | } 84 | } 85 | 86 | #Preview { 87 | WorkaroundView(settingsViewModel: try! SettingsViewModel(workaroundApplications: [ 88 | WorkaroundApplication(bundleIdentifier: "net.mtgto.inputmethod.macSKK", 89 | insertBlankString: true, 90 | icon: NSImage(named: "AppIcon"), displayName: "macSKK"), 91 | WorkaroundApplication(bundleIdentifier: "net.mtgto.inputmethod.macSKK.not-resolved", insertBlankString: false, icon: nil, displayName: nil) 92 | ])) 93 | } 94 | 95 | #Preview("空のとき") { 96 | WorkaroundView(settingsViewModel: try! SettingsViewModel(workaroundApplications: [])) 97 | } 98 | -------------------------------------------------------------------------------- /macSKK/SettingsWatcher.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Cocoa 5 | 6 | /** 7 | * App Sandbox Data ContainerのSettingsフォルダを監視する 8 | * 9 | * 現状はローマ字かな変換ルールファイル (kana-rule.conf) のみを対象としています。 10 | */ 11 | final class SettingsWatcher: NSObject, Sendable { 12 | private let kanaRuleFileName: String 13 | private let settingsDirectoryURL: URL 14 | // MARK: NSFilePresenter 15 | let presentedItemURL: URL? 16 | let presentedItemOperationQueue: OperationQueue = OperationQueue() 17 | 18 | @MainActor init(kanaRuleFileName: String = "kana-rule.conf") throws { 19 | self.kanaRuleFileName = kanaRuleFileName 20 | settingsDirectoryURL = try FileManager.default.url( 21 | for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false 22 | ).appending(path: "Settings") 23 | if !FileManager.default.fileExists(atPath: settingsDirectoryURL.path) { 24 | logger.log("設定フォルダがないため作成します") 25 | try FileManager.default.createDirectory(at: settingsDirectoryURL, withIntermediateDirectories: true) 26 | } 27 | self.presentedItemURL = settingsDirectoryURL 28 | super.init() 29 | let kanaRuleURL = settingsDirectoryURL.appending(path: kanaRuleFileName) 30 | if FileManager.default.fileExists(atPath: kanaRuleURL.path) { 31 | loadKanaRule(contentsOf: kanaRuleURL) 32 | } 33 | NSFileCoordinator.addFilePresenter(self) 34 | } 35 | 36 | deinit { 37 | NSFileCoordinator.removeFilePresenter(self) 38 | } 39 | 40 | @MainActor func loadKanaRule(contentsOf url: URL) { 41 | do { 42 | if try url.isReadable() { 43 | let kanaRule = try Romaji(contentsOf: url) 44 | if kanaRule.isEmpty { 45 | Global.kanaRule = Global.defaultKanaRule 46 | logger.log("ローマ字かな変換ルールファイルが空のためデフォルトのルールを使用します") 47 | } else { 48 | Global.kanaRule = kanaRule 49 | logger.log("独自のローマ字かな変換ルールを適用しました") 50 | } 51 | } else { 52 | logger.log("ローマ字かな変換ルールファイルとして不適合なファイルであるため読み込みできませんでした") 53 | } 54 | } catch { 55 | logger.error("ローマ字かな変換ルールの読み込みでエラーが発生しました: \(error)") 56 | } 57 | } 58 | } 59 | 60 | extension SettingsWatcher: NSFilePresenter { 61 | func presentedSubitemDidAppear(at url: URL) { 62 | if url.lastPathComponent == kanaRuleFileName { 63 | logger.log("ローマ字かな変換ルールファイルが作成されたため読み込みます") 64 | Task { @MainActor in 65 | loadKanaRule(contentsOf: url) 66 | } 67 | } 68 | } 69 | 70 | func presentedSubitemDidChange(at url: URL) { 71 | if url.lastPathComponent == kanaRuleFileName { 72 | // 削除されたときにaccommodatePresentedSubitemDeletionが呼ばれないがこのメソッドは呼ばれるようだった。 73 | // そのためこのメソッドで削除のとき同様の処理を行う。 74 | if !FileManager.default.fileExists(atPath: settingsDirectoryURL.appending(path: kanaRuleFileName).path) { 75 | logger.log("ローマ字かな変換ルールファイルが存在しなくなったためデフォルトのルールに戻します") 76 | Task { @MainActor in 77 | Global.kanaRule = Global.defaultKanaRule 78 | } 79 | return 80 | } 81 | 82 | var relationship: FileManager.URLRelationship = .same 83 | do { 84 | try FileManager.default.getRelationship(&relationship, ofDirectoryAt: settingsDirectoryURL, toItemAt: url) 85 | if case .contains = relationship { 86 | logger.log("ローマ字かな変換ルールファイルが変更されたため読み込みます") 87 | Task { @MainActor in 88 | loadKanaRule(contentsOf: url) 89 | } 90 | } 91 | } catch { 92 | logger.error("ローマ字かな変換ルールファイルが更新されましたが情報取得に失敗しました: \(error)") 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /macSKK/String+Transform.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | extension String { 7 | func toZenkaku() -> String { 8 | guard let converted = applyingTransform(.fullwidthToHalfwidth, reverse: true) else { 9 | fatalError("全角への変換に失敗: \"\(self)\"") 10 | } 11 | return converted 12 | } 13 | 14 | func toHankaku() -> String { 15 | guard let converted = applyingTransform(.fullwidthToHalfwidth, reverse: false) else { 16 | fatalError("半角への変換に失敗: \"\(self)\"") 17 | } 18 | return converted 19 | } 20 | 21 | func toKatakana() -> String { 22 | guard 23 | let converted = replacingOccurrences(of: "う゛", with: "ヴ").applyingTransform( 24 | .hiraganaToKatakana, reverse: false) 25 | else { 26 | fatalError("カタカナへの変換に失敗: \"\(self)\"") 27 | } 28 | return converted 29 | } 30 | 31 | /** 32 | * アルファベットだけで構成されているかを返す。 33 | * 34 | * どちらかに決めないといけないので空文字列はtrue 35 | */ 36 | var isAlphabet: Bool { self.allSatisfy { $0.isAlphabet } } 37 | 38 | /** 39 | * ひらがなだけで構成されているかを返す。 40 | */ 41 | var isHiragana: Bool { self.allSatisfy { $0.isHiragana } } 42 | 43 | /** 44 | * 自身が見出し語のとき、送り仮名ありの見出し語かどうかを返す。 45 | * 46 | * どちらかに決めないといけないので一文字もしくは空文字列はfalse 47 | */ 48 | var isOkuriAri: Bool { 49 | if let first = first, let last = last, count > 1 { 50 | return last.isAlphabet && !first.isAlphabet 51 | } 52 | return false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /macSKK/SystemDict.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | /// Dictionary Serviceを使ってシステム辞書から検索する 7 | @MainActor class SystemDict { 8 | enum Kind: String, CaseIterable, Identifiable { 9 | case daijirin = "com.apple.dictionary.ja.Daijirin" 10 | case wisdom = "com.apple.dictionary.ja-en.WISDOM" 11 | var id: Self { self } 12 | } 13 | 14 | private static let dictionaries: [Kind: DCSDictionary] = { 15 | return Dictionary(Kind.allCases.compactMap { kind in 16 | if let dictionary = findSystemDict(identifier: kind.rawValue) { 17 | return (kind, dictionary) 18 | } else { 19 | return nil 20 | } 21 | }, uniquingKeysWith: { (first, _) in first }) 22 | }() 23 | 24 | class func lookup(_ word: String, for kind: Kind) -> String? { 25 | if let dictionary = dictionaries[kind], let result = DCSCopyTextDefinition(dictionary, word as NSString, CFRangeMake(0, word.count)) { 26 | return result.takeRetainedValue() as String 27 | } 28 | return nil 29 | } 30 | 31 | class func findSystemDict(identifier: String) -> DCSDictionary? { 32 | guard let dictionaries = DCSCopyAvailableDictionaries() as? Set else { 33 | logger.error("システム辞書が見つかりません") 34 | return nil 35 | } 36 | let dictionary = dictionaries.first { 37 | DCSDictionaryGetIdentifier($0) == identifier 38 | } 39 | if let dictionary { 40 | return dictionary 41 | } else { 42 | logger.warning("システム辞書 \(identifier, privacy: .public) が利用可能な辞書にありませんでした") 43 | return nil 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /macSKK/UNNotifier.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | import UserNotifications 6 | 7 | /// 通知センターへの通知処理 8 | struct UNNotifier { 9 | // ユーザー辞書の読み込みエラーの通知センター用通知のID 10 | static let userNotificationReadErrorIdentifier = "net.mtgto.inputmethod.macSKK.userNotification.userDictReadError" 11 | // ユーザー辞書の書き込みエラーの通知センター用通知のID 12 | static let userNotificationWriteErrorIdentifier = "net.mtgto.inputmethod.macSKK.userNotification.userDictWriteError" 13 | 14 | static func sendNotificationForUserDict(readError: any Error) { 15 | let content = UNMutableNotificationContent() 16 | content.title = String(localized: "UNUserDictReadErrorTitle", comment: "エラー") 17 | content.body = String(localized: "UNUserDictReadErrorBody", comment: "ユーザー辞書の読み込みに失敗しました") 18 | 19 | let request = UNNotificationRequest(identifier: Self.userNotificationReadErrorIdentifier, content: content, trigger: nil) 20 | sendUserNotification(request: request) 21 | } 22 | 23 | static func sendNotificationForUserDict(failureEntryCount: Int) { 24 | let content = UNMutableNotificationContent() 25 | content.title = String(localized: "UNUserDictReadFailureEntryTitle", comment: "エラー") 26 | content.body = String(format: String(localized: "UNUserDictReadFailureEntryBody"), failureEntryCount) 27 | 28 | let request = UNNotificationRequest(identifier: Self.userNotificationReadErrorIdentifier, content: content, trigger: nil) 29 | sendUserNotification(request: request) 30 | } 31 | 32 | static func sendNotificationForUserDict(writeError: any Error) { 33 | let content = UNMutableNotificationContent() 34 | content.title = String(localized: "UNUserDictWriteErrorTitle", comment: "エラー") 35 | content.body = String(localized: "UNUserDictWriteErrorBody", comment: "ユーザー辞書の永続化に失敗しました") 36 | 37 | let request = UNNotificationRequest(identifier: Self.userNotificationWriteErrorIdentifier, content: content, trigger: nil) 38 | sendUserNotification(request: request) 39 | } 40 | 41 | private static func sendUserNotification(request: UNNotificationRequest) { 42 | let center = UNUserNotificationCenter.current() 43 | center.requestAuthorization { granted, error in 44 | if let error { 45 | logger.log("通知センターへの通知ができない状態です:\(error)") 46 | return 47 | } 48 | if !granted { 49 | logger.log("通知センターへの通知がユーザーに拒否されています") 50 | return 51 | } 52 | center.add(request) { error in 53 | if let error { 54 | logger.error("通知センターへの通知に失敗しました: \(error)") 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /macSKK/URL+Additions.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | extension URL { 7 | /** 8 | * 読み込み可能なファイルかどうかを返す 9 | * 10 | * 隠しファイルでない、通常ファイルである(シンボリックリンクなどでない)、読み込み可能であるかどうかを調べる 11 | */ 12 | func isReadable() throws -> Bool { 13 | let resourceValues = try resourceValues(forKeys: [.isReadableKey, .isRegularFileKey, .isHiddenKey]) 14 | if let isHidden = resourceValues.isHidden, let isReadable = resourceValues.isReadable, let isRegularFile = resourceValues.isRegularFile { 15 | if isHidden { 16 | return false 17 | } 18 | if !isRegularFile { 19 | return false 20 | } 21 | if !isReadable { 22 | return false 23 | } 24 | } 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /macSKK/UpdateChecker.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | /// GitHubのReleaseページにあるatom+xmlをFetchUpdateService XPC取得し、パースした結果を返します。 7 | struct UpdateChecker { 8 | // atomをパースして返すので新しい順で返す 9 | func fetch() async throws -> Release { 10 | let service = NSXPCConnection(serviceName: "net.mtgto.inputmethod.macSKK.FetchUpdateService") 11 | service.remoteObjectInterface = NSXPCInterface(with: (any FetchUpdateServiceProtocol).self) 12 | service.resume() 13 | 14 | defer { 15 | service.invalidate() 16 | } 17 | 18 | guard let proxy = service.remoteObjectProxy as? any FetchUpdateServiceProtocol else { 19 | throw FetchUpdateServiceError.invalidProxy 20 | } 21 | let response = try await proxy.fetch() 22 | let decoder = JSONDecoder() 23 | decoder.dateDecodingStrategy = .iso8601 24 | return try decoder.decode(Release.self, from: response) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /macSKK/UserNotificationDelegate.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import AppKit 5 | import UserNotifications 6 | 7 | class UserNotificationDelegate: NSObject, UNUserNotificationCenterDelegate { 8 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 9 | if response.notification.request.identifier == Release.userNotificationIdentifier { 10 | if let userInfo = response.notification.request.content.userInfo[Release.userNotificationUserInfoKey] as? [String: Any], 11 | let urlString = userInfo[Release.userNotificationUserInfoNameUrl] as? String, 12 | let url = URL(string: urlString) { 13 | // リリースページを開く 14 | if !NSWorkspace.shared.open(url) { 15 | logger.warning("新しいバージョンの通知をタップしたがリリースページを開くことができませんでした。") 16 | } 17 | } else { 18 | logger.error("通知メッセージにリリースページの情報が含まれていません。バグの可能性が高いです") 19 | } 20 | } 21 | completionHandler() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /macSKK/View/AnnotationView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | /// 注釈表示 7 | struct AnnotationView: View { 8 | @Binding var annotations: [Annotation] 9 | @Binding var systemAnnotation: String? 10 | let annotationFontSize: CGFloat 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | var body: some View { 14 | VStack(alignment: .leading) { 15 | ScrollView { 16 | if let systemAnnotation { 17 | HStack(alignment: .top) { 18 | VStack(alignment: .leading) { 19 | Text("System Dict") 20 | .font(.system(size: annotationFontSize, weight: .bold)) 21 | Text(systemAnnotation) 22 | .textSelection(.enabled) 23 | .font(.system(size: annotationFontSize)) 24 | // ↓ ダークモードではテキスト選択時に文字色が白から黒に変わってしまう問題があるので暫定対処 25 | .foregroundColor(colorScheme == .dark ? .white : nil) 26 | .fixedSize(horizontal: false, vertical: true) 27 | .layoutPriority(1) 28 | .padding(.leading) 29 | } 30 | Spacer() 31 | } 32 | } 33 | HStack(alignment: .top) { 34 | VStack(alignment: .leading) { 35 | ForEach(annotations, id: \.dictId) { annotation in 36 | Text(annotation.dictId) 37 | .font(.system(size: annotationFontSize, weight: .bold)) 38 | Text(annotation.text) 39 | .textSelection(.enabled) 40 | .font(.system(size: annotationFontSize, weight: .regular)) 41 | .foregroundColor(colorScheme == .dark ? .white : nil) 42 | .fixedSize(horizontal: false, vertical: true) 43 | .layoutPriority(1) 44 | .padding(.leading) 45 | } 46 | } 47 | Spacer() 48 | } 49 | } 50 | .scrollBounceBehavior(.basedOnSize) 51 | } 52 | } 53 | } 54 | 55 | struct AnnotationView_Previews: PreviewProvider { 56 | static let annotationFontSize = CGFloat(13) 57 | static var previews: some View { 58 | AnnotationView( 59 | annotations: .constant([Annotation(dictId: "SKK-JISYO.L", text: "これは辞書の注釈です。")]), 60 | systemAnnotation: .constant(nil), 61 | annotationFontSize: annotationFontSize 62 | ) 63 | .frame(width: 300) 64 | .previewDisplayName("SKK辞書の注釈のみ") 65 | AnnotationView( 66 | annotations: .constant([Annotation(dictId: "SKK-JISYO.L", text: "これは辞書の注釈です。"), 67 | Annotation(dictId: Annotation.userDictId, text: "これはユーザー辞書の注釈です。")]), 68 | systemAnnotation: .constant(nil), 69 | annotationFontSize: annotationFontSize 70 | ) 71 | .frame(width: 300) 72 | .previewDisplayName("SKK辞書の注釈 + ユーザー辞書の注釈") 73 | AnnotationView( 74 | annotations: .constant([Annotation(dictId: "SKK-JISYO.L", text: "これは辞書の注釈です。")]), 75 | systemAnnotation: .constant(String(repeating: "これはシステム辞書の注釈です。", count: 10)), 76 | annotationFontSize: annotationFontSize 77 | ) 78 | .frame(width: 300) 79 | .previewDisplayName("SKK辞書の注釈 & システム辞書の注釈") 80 | AnnotationView( 81 | annotations: .constant([]), 82 | systemAnnotation: .constant(String(repeating: "これはシステム辞書の注釈です。", count: 10)), 83 | annotationFontSize: annotationFontSize 84 | ) 85 | .frame(width: 300) 86 | .previewDisplayName("システム辞書のみ") 87 | AnnotationView( 88 | annotations: .constant([]), 89 | systemAnnotation: .constant(nil), 90 | annotationFontSize: annotationFontSize 91 | ) 92 | .frame(width: 300) 93 | .previewDisplayName("注釈なし") 94 | AnnotationView( 95 | annotations: .constant([Annotation(dictId: "SKK-JISYO.L", text: "フォントサイズ19")]), 96 | systemAnnotation: .constant(nil), 97 | annotationFontSize: CGFloat(19) 98 | ) 99 | .frame(width: 300) 100 | .previewDisplayName("フォントサイズ19") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /macSKK/View/CandidatesPanel.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Cocoa 5 | import SwiftUI 6 | 7 | /// 変換候補リストをフローティングモーダルで表示するパネル 8 | @MainActor 9 | final class CandidatesPanel: NSPanel { 10 | let viewModel: CandidatesViewModel 11 | var cursorPosition: NSRect = .zero 12 | 13 | /** 14 | * - Parameters: 15 | * - showAnnotationPopover: パネル表示時に注釈を表示するかどうか 16 | * - candidatesFontSize: 変換候補のフォントサイズ 17 | */ 18 | init(showAnnotationPopover: Bool, candidatesFontSize: Int, annotationFontSize: Int) { 19 | viewModel = CandidatesViewModel(candidates: [], 20 | currentPage: 0, 21 | totalPageCount: 0, 22 | showAnnotationPopover: showAnnotationPopover, 23 | candidatesFontSize: CGFloat(candidatesFontSize), 24 | annotationFontSize: CGFloat(annotationFontSize)) 25 | let rootView = CandidatesView(candidates: self.viewModel) 26 | let viewController = NSHostingController(rootView: rootView) 27 | // borderlessにしないとdeactivateServerが呼ばれてしまう 28 | super.init(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: true) 29 | backgroundColor = .clear 30 | contentViewController = viewController 31 | // フルキーボードアクセスが有効なときに変換パネルが表示されなくなるのを回避 32 | setAccessibilityElement(false) 33 | } 34 | 35 | func setCandidates(_ candidates: CurrentCandidates, selected: Candidate?) { 36 | viewModel.selected = selected 37 | viewModel.candidates = candidates 38 | } 39 | 40 | func setSystemAnnotation(_ systemAnnotation: String, for word: Word.Word) { 41 | viewModel.systemAnnotations[word] = systemAnnotation 42 | } 43 | 44 | func setCursorPosition(_ cursorPosition: NSRect) { 45 | self.cursorPosition = cursorPosition 46 | if let mainScreen = NSScreen.main { 47 | viewModel.maxWidth = mainScreen.visibleFrame.minX + mainScreen.visibleFrame.size.width - cursorPosition.origin.x 48 | viewModel.maxHeight = cursorPosition.origin.y - mainScreen.visibleFrame.minY 49 | } 50 | } 51 | 52 | func setShowAnnotationPopover(_ showAnnotationPopover: Bool) { 53 | self.viewModel.showAnnotationPopover = showAnnotationPopover 54 | } 55 | 56 | func setCandidatesFontSize(_ candidatesFontSize: Int) { 57 | self.viewModel.candidatesFontSize = CGFloat(candidatesFontSize) 58 | } 59 | 60 | func setAnnotationFontSize(_ annotationFontSize: Int) { 61 | self.viewModel.annotationFontSize = CGFloat(annotationFontSize) 62 | } 63 | 64 | /** 65 | * 表示する。スクリーンからはみ出す位置が指定されている場合は自動で調整する。 66 | * 67 | * - 下にはみ出る場合: テキストの上側に表示する 68 | * - 右にはみ出す場合: スクリーン右端に接するように表示する 69 | */ 70 | func show(windowLevel: NSWindow.Level) { 71 | // 原因は特定できてないが特殊な場合 (終了が要求されているときなど?) で下記の分岐がfalseになるケースがあったので対処 72 | guard let viewController = contentViewController as? NSHostingController else { 73 | logger.error("ビューコントローラの状態が想定と異なるため変換候補パネルを表示できません") 74 | return 75 | } 76 | #if DEBUG 77 | print("content size = \(viewController.sizeThatFits(in: CGSize(width: Int.max, height: Int.max)))") 78 | print("intrinsicContentSize = \(viewController.view.intrinsicContentSize)") 79 | print("frame = \(frame)") 80 | print("preferredContentSize = \(viewController.preferredContentSize)") 81 | print("sizeThatFits = \(viewController.sizeThatFits(in: CGSize(width: 10000, height: 10000)))") 82 | #endif 83 | var origin = cursorPosition.origin 84 | let width: CGFloat 85 | let height: CGFloat 86 | if case let .panel(words, _, _) = viewModel.candidates { 87 | switch Global.candidateListDirection.value { 88 | case .vertical: 89 | width = viewModel.showAnnotationPopover ? viewModel.minWidth + CandidatesView.annotationPopupWidth : viewModel.minWidth 90 | height = CGFloat(words.count) * viewModel.candidatesLineHeight + CandidatesView.footerHeight 91 | if viewModel.displayPopoverInLeftOrTop { 92 | origin.x = origin.x - CandidatesView.annotationPopupWidth - CandidatesView.annotationMarginLeftRight 93 | } 94 | case .horizontal: 95 | width = viewModel.minWidth 96 | height = (viewModel.showAnnotationPopover ? HorizontalCandidatesView.annotationPopupHeight + CandidatesView.annotationMarginTopBottom : 0) + viewModel.candidatesLineHeight 97 | } 98 | } else { 99 | // FIXME: 短い文のときにはそれに合わせて高さを縮める 100 | width = viewModel.minWidth 101 | height = 200 102 | } 103 | setContentSize(NSSize(width: width, height: height)) 104 | if let mainScreen = NSScreen.main { 105 | let visibleFrame = mainScreen.visibleFrame 106 | if origin.x + width > visibleFrame.minX + visibleFrame.width { 107 | origin.x = visibleFrame.minX + visibleFrame.width - width 108 | } 109 | if origin.y - height < visibleFrame.minY { 110 | origin.y = cursorPosition.maxY + height 111 | } 112 | } 113 | setFrameTopLeftPoint(origin) 114 | level = windowLevel 115 | orderFrontRegardless() 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /macSKK/View/CandidatesView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | /// 変換候補ビュー 7 | /// とりあえず10件ずつ縦に表示、スペースで次の10件が表示される 8 | struct CandidatesView: View { 9 | @ObservedObject var candidates: CandidatesViewModel 10 | static let footerHeight: CGFloat = 20 11 | /// 縦表示の変換候補と注釈の間 (左右の余白) 12 | static let annotationMarginLeftRight: CGFloat = 8 13 | /// 横表示の変換候補と注釈の間 (上下の余白) 14 | static let annotationMarginTopBottom: CGFloat = 4 15 | /// パネル型の注釈ビューの幅 16 | static let annotationPopupWidth: CGFloat = 300 17 | 18 | var body: some View { 19 | switch candidates.candidates { 20 | case .inline: 21 | AnnotationView( 22 | annotations: $candidates.selectedAnnotations, 23 | systemAnnotation: $candidates.selectedSystemAnnotation, 24 | annotationFontSize: candidates.annotationFontSize 25 | ) 26 | .padding(EdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)) 27 | .frame(width: 300, height: 200) 28 | .background() 29 | case let .panel(words, currentPage, totalPageCount): 30 | switch Global.candidateListDirection.value { 31 | case .vertical: 32 | VerticalCandidatesView(candidates: candidates, words: words, currentPage: currentPage, totalPageCount: totalPageCount) 33 | case .horizontal: 34 | HorizontalCandidatesView(candidates: candidates, words: words, currentPage: currentPage, totalPageCount: totalPageCount) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /macSKK/View/CompletionPanel.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Cocoa 5 | import SwiftUI 6 | 7 | class CompletionPanel: NSPanel { 8 | let viewModel: CompletionViewModel 9 | 10 | init() { 11 | viewModel = CompletionViewModel(completion: "") 12 | let rootView = CompletionView(viewModel: viewModel) 13 | let viewController = NSHostingController(rootView: rootView) 14 | super.init(contentRect: .zero, styleMask: [.nonactivatingPanel], backing: .buffered, defer: true) 15 | contentViewController = viewController 16 | } 17 | 18 | func show(at cursorPoint: NSRect, windowLevel: NSWindow.Level) { 19 | level = windowLevel 20 | var origin = cursorPoint.origin 21 | 22 | if let size = contentViewController?.view.frame.size, let mainScreen = NSScreen.main { 23 | let visibleFrame = mainScreen.visibleFrame 24 | if origin.x + size.width > visibleFrame.minX + visibleFrame.width { 25 | origin.x = visibleFrame.minX + visibleFrame.width - size.width 26 | } 27 | // 1ピクセルの余白を設ける 28 | if origin.y - size.height < visibleFrame.minY { 29 | origin.y = origin.y + size.height + cursorPoint.height + 1 30 | } else { 31 | origin.y -= 1 32 | } 33 | } 34 | setFrameTopLeftPoint(origin) 35 | orderFrontRegardless() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /macSKK/View/CompletionView.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import SwiftUI 5 | 6 | /// 補完候補を表示するビュー 7 | struct CompletionView: View { 8 | @ObservedObject var viewModel: CompletionViewModel 9 | 10 | var body: some View { 11 | VStack { 12 | Text(viewModel.completion) 13 | .font(.body) 14 | Text("Tab Completion") 15 | .font(.caption) 16 | .frame(maxWidth: .infinity) 17 | } 18 | .padding(2) 19 | .fixedSize() 20 | } 21 | } 22 | 23 | struct CompletionView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | CompletionView(viewModel: CompletionViewModel(completion: "あいうえおかきくけこさしすせそ")) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /macSKK/View/CompletionViewModel.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Combine 5 | 6 | @MainActor 7 | final class CompletionViewModel: ObservableObject { 8 | @Published var completion: String = "" 9 | 10 | init(completion: String) { 11 | self.completion = completion 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /macSKK/View/InputModePanel.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Cocoa 5 | 6 | /// 入力モードをフローティングモーダルで表示するパネル 7 | @MainActor 8 | class InputModePanel: NSPanel { 9 | private let imageView: NSImageView 10 | private let imageSize: CGSize 11 | 12 | init() { 13 | imageSize = CGSize(width: 33, height: 24) 14 | imageView = NSImageView(frame: .zero) 15 | super.init(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: true) 16 | imageView.imageScaling = .scaleProportionallyUpOrDown 17 | backgroundColor = .clear 18 | isOpaque = false 19 | ignoresMouseEvents = true 20 | hasShadow = false 21 | contentView = imageView 22 | setContentSize(imageSize) 23 | } 24 | 25 | func show(at point: NSPoint, mode: InputMode, privateMode: Bool, windowLevel: NSWindow.Level) { 26 | // 画像の高さ分だけ下にずらす 27 | let origin = NSPoint(x: point.x, y: point.y - imageSize.height) 28 | let rect = NSRect(origin: origin, size: imageSize) 29 | setFrame(rect, display: true) 30 | level = windowLevel 31 | switch mode { 32 | case .hiragana: 33 | imageView.image = NSImage(named: privateMode ? "icon-hiragana-locked" : "icon-hiragana") 34 | case .katakana: 35 | imageView.image = NSImage(named: privateMode ? "icon-katakana-locked" : "icon-katakana") 36 | case .hankaku: 37 | imageView.image = NSImage(named: privateMode ? "icon-hankaku-locked" : "icon-hankaku") 38 | case .eisu: 39 | imageView.image = NSImage(named: privateMode ? "icon-eisu-locked" : "icon-eisu") 40 | case .direct: 41 | imageView.image = NSImage(named: privateMode ? "icon-direct-locked" : "icon-direct") 42 | } 43 | 44 | alphaValue = 1.0 45 | // Note: orderFront(nil) だと "Warning: Window NSWindow 0x13b72dbe0 ordered front from a non-active application 46 | // and may order beneath the active application's windows." のようなエラーがConsole.appに出力される 47 | // orderFrontRegardlessだとそのようなログが出ない 48 | orderFrontRegardless() 49 | // フェードアウト 50 | NSAnimationContext.runAnimationGroup { context in 51 | context.duration = 2.0 52 | context.timingFunction = CAMediaTimingFunction(name: .easeIn) 53 | self.animator().alphaValue = 0.0 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /macSKK/View/SettingsWindow.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Cocoa 5 | import SwiftUI 6 | 7 | class SettingsWindow: NSWindow { 8 | init(settingsViewModel: SettingsViewModel) { 9 | let rootView = SettingsView(settingsViewModel: settingsViewModel) 10 | let viewController = NSHostingController(rootView: rootView) 11 | super.init(contentRect: .zero, styleMask: [.titled, .closable, .fullSizeContentView, .unifiedTitleAndToolbar], backing: .buffered, defer: true) 12 | contentViewController = viewController 13 | isExcludedFromWindowsMenu = true 14 | center() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /macSKK/Word.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | 6 | /// 見出し語の注釈 7 | struct Annotation: Equatable { 8 | static let userDictId: FileDict.ID = String(localized: "User Dictionary", comment: "ユーザー辞書") 9 | /// 注釈がつけられた辞書のID。現状はユーザー辞書以外はファイル名になっておりSKK-JISYO.Lなどになる。 10 | let dictId: FileDict.ID 11 | let text: String 12 | } 13 | 14 | /// 辞書に登録する言葉。 15 | /// 16 | /// - NOTE: 将来プログラム辞書みたいな機能が増えるかもしれない。 17 | struct Word: Hashable { 18 | typealias Word = String 19 | let word: Word 20 | /// 送り仮名ブロックのひらがな ("った" のように2文字以上のことがある) 21 | let okuri: String? 22 | let annotation: Annotation? 23 | 24 | init(_ word: Self.Word, okuri: String? = nil, annotation: Annotation? = nil) { 25 | self.word = word 26 | self.annotation = annotation 27 | self.okuri = okuri 28 | } 29 | 30 | func hash(into hasher: inout Hasher) { 31 | hasher.combine(word) 32 | if let okuri { 33 | hasher.combine(okuri) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /macSKK/direct.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/direct.tiff -------------------------------------------------------------------------------- /macSKK/eisu.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/eisu.tiff -------------------------------------------------------------------------------- /macSKK/hankaku.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/hankaku.tiff -------------------------------------------------------------------------------- /macSKK/hiragana.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/hiragana.tiff -------------------------------------------------------------------------------- /macSKK/kana-rule.conf: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ひらかな・カタカナ・半角カナ入力モードでのキー入力変換マップ 3 | # UTF-8 + LF (BOMなし) で記述してください。 4 | # #で始まる行は無視されます。 5 | 6 | # 自分でカスタマイズして使用したい場合はこのファイルを 7 | # ~/Library/Containers/net.mtgto.inputmethod.macSKK/Data/Documents/Settings/kana-rule.conf 8 | # に置いてください。 9 | 10 | # 各行はカンマ区切りで2-5要素記述する必要があります。 11 | # 各要素でカンマを使用したい場合は , と記述してください。 12 | # 第一要素でシャープを使用したい場合は ♯ と記述してください。 13 | # 1つ目: ローマ字入力で確定入力されるまでに入力される文字列を指定してください。 14 | # 2つ目: ひらがなモードで入力される文字を指定してください。 15 | # 3つ目: カタカナモードで入力される文字を指定してください。 16 | # 省略時はひらがなモードで入力される文字を自動でカタカナに変換します。 17 | # 記号などはカタカナに変換できないのでそのまま使用されます。 18 | # 4つ目: 半角カナ入力モードで入力される文字を指定してください。 19 | # 省略時はひらがなモードで入力される文字を自動で半角に変換します。 20 | # 5つ目: 未確定のローマ字として残す文字を指定してください。 21 | # 省略された場合は未確定文字として残るローマ字がないことを表します。 22 | # 例えば "tt,っ,ッ,ッ,t" という行がある場合、 23 | # "tt" と入力したときには "っ" をひらがなモードで入力し "t" が未確定のローマ字として残ります。 24 | 25 | 26 | # ローマ字入力 27 | # JIS X 4063:2000 (廃止済) で規定されていたローマ字入力を基にしています。 28 | # https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%BC%E3%83%9E%E5%AD%97%E5%85%A5%E5%8A%9B 29 | a,あ 30 | i,い 31 | u,う 32 | e,え 33 | o,お 34 | ka,か 35 | ki,き 36 | ku,く 37 | ke,け 38 | ko,こ 39 | sa,さ 40 | si,し 41 | shi,し 42 | su,す 43 | se,せ 44 | so,そ 45 | ta,た 46 | ti,ち 47 | chi,ち 48 | tu,つ 49 | tsu,つ 50 | te,て 51 | to,と 52 | na,な 53 | ni,に 54 | nu,ぬ 55 | ne,ね 56 | no,の 57 | ha,は 58 | hi,ひ 59 | hu,ふ 60 | fu,ふ 61 | he,へ 62 | ho,ほ 63 | ma,ま 64 | mi,み 65 | mu,む 66 | me,め 67 | mo,も 68 | ya,や 69 | yu,ゆ 70 | yo,よ 71 | ra,ら 72 | ri,り 73 | ru,る 74 | re,れ 75 | ro,ろ 76 | wa,わ 77 | wyi,ゐ 78 | wye,ゑ 79 | wo,を 80 | n,ん 81 | nn,ん 82 | ga,が 83 | gi,ぎ 84 | gu,ぐ 85 | ge,げ 86 | go,ご 87 | za,ざ 88 | zi,じ 89 | ji,じ 90 | zu,ず 91 | ze,ぜ 92 | zo,ぞ 93 | da,だ 94 | di,ぢ 95 | du,づ 96 | de,で 97 | do,ど 98 | ba,ば 99 | bi,び 100 | bu,ぶ 101 | be,べ 102 | bo,ぼ 103 | pa,ぱ 104 | pi,ぴ 105 | pu,ぷ 106 | pe,ぺ 107 | po,ぽ 108 | kya,きゃ 109 | kyu,きゅ 110 | kyo,きょ 111 | sya,しゃ 112 | sha,しゃ 113 | syu,しゅ 114 | shu,しゅ 115 | syo,しょ 116 | sho,しょ 117 | tya,ちゃ 118 | cha,ちゃ 119 | tyu,ちゅ 120 | chu,ちゅ 121 | tyo,ちょ 122 | cho,ちょ 123 | nya,にゃ 124 | nyu,にゅ 125 | nyo,にょ 126 | hya,ひゃ 127 | hyu,ひゅ 128 | hyo,ひょ 129 | mya,みゃ 130 | myu,みゅ 131 | myo,みょ 132 | rya,りゃ 133 | ryu,りゅ 134 | ryo,りょ 135 | gya,ぎゃ 136 | gyu,ぎゅ 137 | gyo,ぎょ 138 | zya,じゃ 139 | ja,じゃ 140 | zyu,じゅ 141 | ju,じゅ 142 | zyo,じょ 143 | jo,じょ 144 | dya,ぢゃ 145 | dyu,ぢゅ 146 | dyo,ぢょ 147 | bya,びゃ 148 | byu,びゅ 149 | byo,びょ 150 | pya,ぴゃ 151 | pyu,ぴゅ 152 | pyo,ぴょ 153 | sye,しぇ 154 | she,しぇ 155 | tye,ちぇ 156 | che,ちぇ 157 | tsa,つぁ 158 | tse,つぇ 159 | tso,つぉ 160 | thi,てぃ 161 | fa,ふぁ 162 | fi,ふぃ 163 | fe,ふぇ 164 | fo,ふぉ 165 | zye,じぇ 166 | je,じぇ 167 | dye,ぢぇ 168 | dhi,でぃ 169 | dhu,でゅ 170 | xa,ぁ 171 | xi,ぃ 172 | xu,ぅ 173 | xe,ぇ 174 | xo,ぉ 175 | xka,ヵ 176 | xke,ヶ 177 | xtu,っ 178 | xya,ゃ 179 | xyu,ゅ 180 | xyo,ょ 181 | xwa,ゎ 182 | # 追加で実装したほうがよい入力方法 183 | ye,いぇ 184 | whi,うぃ 185 | wi,うぃ 186 | whe,うぇ 187 | we,うぇ 188 | who,うぉ 189 | va,ゔぁ,ヴァ,ヴァ 190 | vi,ゔぃ,ヴィ,ヴィ 191 | vu,ゔ,ヴ,ヴ 192 | ve,ゔぇ,ヴェ,ヴェ 193 | vo,ゔぉ,ヴォ,ヴォ 194 | vyu,ゔゅ,ヴュ,ヴュ 195 | kwa,くぁ 196 | kwi,くぃ 197 | kwe,くぇ 198 | kwo,くぉ 199 | gwa,ぐぁ 200 | jya,じゃ 201 | jyu,じゅ 202 | jyo,じょ 203 | cya,ちゃ 204 | cyu,ちゅ 205 | cyo,ちょ 206 | tsi,つぃ 207 | thu,てゅ 208 | twu,とぅ 209 | dwu,どぅ 210 | hwa,ふぁ 211 | hwi,ふぃ 212 | hwe,ふぇ 213 | hwo,ふぉ 214 | fwu,ふゅ 215 | xtsu,っ 216 | # 促音 217 | kk,っ,ッ,ッ,k 218 | ss,っ,ッ,ッ,s 219 | tt,っ,ッ,ッ,t 220 | cc,っ,ッ,ッ,c 221 | hh,っ,ッ,ッ,h 222 | ff,っ,ッ,ッ,f 223 | mm,っ,ッ,ッ,m 224 | yy,っ,ッ,ッ,y 225 | rr,っ,ッ,ッ,r 226 | ww,っ,ッ,ッ,w 227 | gg,っ,ッ,ッ,g 228 | zz,っ,ッ,ッ,z 229 | jj,っ,ッ,ッ,j 230 | dd,っ,ッ,ッ,d 231 | bb,っ,ッ,ッ,b 232 | pp,っ,ッ,ッ,p 233 | 234 | # 全角で入力したい記号 235 | -,ー 236 | ,,、 237 | .,。 238 | [,「 239 | ],」 240 | 241 | # 特殊な入力 242 | z-,〜 243 | z,,‥ 244 | z.,… 245 | z/,・ 246 | zh,← 247 | zj,↓ 248 | zk,↑ 249 | zl,→ 250 | z ,  251 | z(,( 252 | z),) 253 | z[,『 254 | z],』 255 | 256 | # 2つ目の要素を "" + 入力したいキーで書くと、特殊な設定としてシフトキーを押しながら入力したと見做す 257 | # JIS配列の "+" や英字配列の ":" のように、シフトキーで変わるキーを元のキーをシフト入力したと見做すことができる 258 | # 例えばAZIKで ; を「っ」入力に割り当てている場合に下記のような設定をすることで送り仮名の「っ」を入力できる 259 | #+,; 260 | #:,; 261 | -------------------------------------------------------------------------------- /macSKK/katakana.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKK/katakana.tiff -------------------------------------------------------------------------------- /macSKK/macSKK-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | // 4 | // Use this file to import your target's public headers that you would like to expose to Swift. 5 | 6 | #import "DictionaryServiceExtention.h" 7 | -------------------------------------------------------------------------------- /macSKK/macSKK.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.temporary-exception.mach-register.global-name 8 | net.mtgto.inputmethod.macSKK_Connection 9 | 10 | 11 | -------------------------------------------------------------------------------- /macSKKTests/CandidateTest.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class CandidateTest: XCTestCase { 9 | func testAppendAnnotations() throws { 10 | var candidate = Candidate("注釈") 11 | XCTAssertEqual(candidate.annotations, []) 12 | let annotation1 = Annotation(dictId: "d1", text: "a1") 13 | candidate.appendAnnotations([annotation1]) 14 | XCTAssertEqual(candidate.annotations, [annotation1]) 15 | let annotation2 = Annotation(dictId: "d2", text: "a1") 16 | candidate.appendAnnotations([annotation2]) 17 | XCTAssertEqual(candidate.annotations, [annotation1], "注釈が同じテキストなら追加されない") 18 | let annotation3 = Annotation(dictId: "d3", text: "a3") 19 | candidate.appendAnnotations([annotation3]) 20 | XCTAssertEqual(candidate.annotations, [annotation1, annotation3]) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /macSKKTests/Character+AdditionsTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class CharacterAdditionsTests: XCTestCase { 9 | func testIsHiragana() { 10 | XCTAssertTrue(Character("あ").isHiragana) 11 | XCTAssertTrue(Character("ぁ").isHiragana) 12 | XCTAssertTrue(Character("っ").isHiragana) 13 | XCTAssertTrue(Character("ゔ").isHiragana) 14 | XCTAssertTrue(Character("ん").isHiragana) 15 | XCTAssertFalse(Character("ー").isHiragana) 16 | XCTAssertFalse(Character("ア").isHiragana) 17 | XCTAssertFalse(Character("ア").isHiragana) 18 | XCTAssertFalse(Character("a").isHiragana) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /macSKKTests/Character+KeyCode.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import Foundation 5 | import Carbon.HIToolbox.Events 6 | 7 | extension Character { 8 | var keyCode: UInt16? { 9 | switch self { 10 | case "a": 11 | return UInt16(kVK_ANSI_A) 12 | case "b": 13 | return UInt16(kVK_ANSI_B) 14 | case "c": 15 | return UInt16(kVK_ANSI_C) 16 | case "d": 17 | return UInt16(kVK_ANSI_D) 18 | case "e": 19 | return UInt16(kVK_ANSI_E) 20 | case "f": 21 | return UInt16(kVK_ANSI_F) 22 | case "g": 23 | return UInt16(kVK_ANSI_G) 24 | case "h": 25 | return UInt16(kVK_ANSI_H) 26 | case "i": 27 | return UInt16(kVK_ANSI_I) 28 | case "j": 29 | return UInt16(kVK_ANSI_J) 30 | case "k": 31 | return UInt16(kVK_ANSI_K) 32 | case "l": 33 | return UInt16(kVK_ANSI_L) 34 | case "m": 35 | return UInt16(kVK_ANSI_M) 36 | case "n": 37 | return UInt16(kVK_ANSI_N) 38 | case "o": 39 | return UInt16(kVK_ANSI_O) 40 | case "p": 41 | return UInt16(kVK_ANSI_P) 42 | case "q": 43 | return UInt16(kVK_ANSI_Q) 44 | case "r": 45 | return UInt16(kVK_ANSI_R) 46 | case "s": 47 | return UInt16(kVK_ANSI_S) 48 | case "t": 49 | return UInt16(kVK_ANSI_T) 50 | case "u": 51 | return UInt16(kVK_ANSI_U) 52 | case "v": 53 | return UInt16(kVK_ANSI_V) 54 | case "w": 55 | return UInt16(kVK_ANSI_W) 56 | case "x": 57 | return UInt16(kVK_ANSI_X) 58 | case "y": 59 | return UInt16(kVK_ANSI_Y) 60 | case "z": 61 | return UInt16(kVK_ANSI_Z) 62 | case ";": 63 | return UInt16(kVK_ANSI_Semicolon) 64 | case "/": 65 | return UInt16(kVK_ANSI_Slash) 66 | case " ": 67 | return UInt16(kVK_Space) 68 | default: 69 | return nil 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /macSKKTests/CurrentInputTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class CurrentInputTests: XCTestCase { 9 | func generateKeyEvent(modifierFlags: NSEvent.ModifierFlags, characters: String, charactersIgnoringModifiers: String? = nil, keyCode: UInt16 = 0) -> NSEvent { 10 | NSEvent.keyEvent(with: .keyDown, 11 | location: .zero, 12 | modifierFlags: modifierFlags, 13 | timestamp: 0, 14 | windowNumber: 0, 15 | context: nil, 16 | characters: characters, 17 | charactersIgnoringModifiers: charactersIgnoringModifiers ?? characters, 18 | isARepeat: false, 19 | keyCode: keyCode)! 20 | } 21 | 22 | func testCurrentInputCharacterWithShift() { 23 | let inputQ = CurrentInput(key: .character("q"), modifierFlags: []) 24 | let inputShiftQ = CurrentInput(key: .character("q"), modifierFlags: .shift) 25 | let inputShift1 = CurrentInput(key: .character("1"), modifierFlags: .shift) 26 | let eventQ = generateKeyEvent(modifierFlags: [], characters: "q") 27 | let eventShiftQ = generateKeyEvent(modifierFlags: .shift, characters: "q") 28 | let eventShift1 = generateKeyEvent(modifierFlags: .shift, characters: "!", charactersIgnoringModifiers: "1") 29 | let affix = generateKeyEvent(modifierFlags: .shift, characters: ">", charactersIgnoringModifiers: ".") 30 | 31 | XCTAssertEqual(inputQ, CurrentInput(event: eventQ)) 32 | XCTAssertEqual(inputShiftQ, CurrentInput(event: eventShiftQ)) 33 | XCTAssertNotEqual(inputQ, CurrentInput(event: eventShiftQ)) 34 | XCTAssertNotEqual(inputShiftQ, CurrentInput(event: eventQ)) 35 | XCTAssertEqual(inputShift1, CurrentInput(event: eventShift1)) 36 | XCTAssertEqual(CurrentInput(event: affix).key, .character(".")) 37 | } 38 | 39 | func testCurrentInputKeyCodeWithShift() { 40 | let inputLeft = CurrentInput(key: .code(0x7b), modifierFlags: .function) 41 | let inputShiftLeft = CurrentInput(key: .code(0x7b), modifierFlags: [.function, .shift]) 42 | let eventLeft = generateKeyEvent(modifierFlags: [.function], characters: "\u{63234}", keyCode: 0x7b) 43 | let eventShiftLeft = generateKeyEvent(modifierFlags: [.function, .shift], characters: "\u{63234}", keyCode: 0x7b) 44 | 45 | XCTAssertEqual(inputLeft, CurrentInput(event: eventLeft)) 46 | XCTAssertEqual(inputShiftLeft, CurrentInput(event: eventShiftLeft)) 47 | XCTAssertNotEqual(inputLeft, CurrentInput(event: eventShiftLeft)) 48 | XCTAssertNotEqual(inputShiftLeft, CurrentInput(event: eventLeft)) 49 | } 50 | 51 | func testCurrentInputKeyCode() { 52 | let inputEnter = CurrentInput(key: .code(0x24), modifierFlags: []) 53 | let eventEnter = generateKeyEvent(modifierFlags: [], characters: "\r", keyCode: 0x24) 54 | let eventOptionEnter = generateKeyEvent(modifierFlags: .option, characters: "\r", keyCode: 0x24) 55 | 56 | XCTAssertEqual(inputEnter, CurrentInput(event: eventEnter)) 57 | XCTAssertNotEqual(inputEnter, CurrentInput(event: eventOptionEnter)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /macSKKTests/Data+EucJis2004Tests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class URLEucJis2004Tests: XCTestCase { 9 | func testLoad() throws { 10 | let fileURL = Bundle(for: Self.self).url(forResource: "euc-jis-2004", withExtension: "txt")! 11 | let data = try Data(contentsOf: fileURL) 12 | XCTAssertEqual(try data.eucJis2004String(), "川﨑") 13 | } 14 | 15 | func testLoadFail() throws { 16 | let fileURL = Bundle(for: Self.self).url(forResource: "SKK-JISYO.test", withExtension: "utf8")! 17 | let data = try Data(contentsOf: fileURL) 18 | XCTAssertThrowsError(try data.eucJis2004String()) { 19 | XCTAssertEqual($0 as! EucJis2004Error, EucJis2004Error.convert) 20 | } 21 | } 22 | 23 | func testLoadEmpty() throws { 24 | let fileURL = Bundle(for: Self.self).url(forResource: "empty", withExtension: "txt")! 25 | let data = try Data(contentsOf: fileURL) 26 | XCTAssertEqual(try data.eucJis2004String(), "") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /macSKKTests/EntryTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class EntryTests: XCTestCase { 9 | func testInit() throws { 10 | let entry = Entry(line: "あg /挙/揚/上/", dictId: "") 11 | XCTAssertEqual(entry?.yomi, "あg") 12 | XCTAssertEqual(entry?.candidates.map { $0.word }, ["挙", "揚", "上"]) 13 | } 14 | 15 | func testDecode() { 16 | XCTAssertEqual(Entry(line: #"ao /(concat "and\057or")/"#, dictId: "")?.candidates.first?.word, "and/or") 17 | XCTAssertEqual(Entry(line: #"ao /(concat "and\073or")/"#, dictId: "")?.candidates.first?.word, "and;or") 18 | } 19 | 20 | func testAnnotation() throws { 21 | guard let entry = Entry(line: "けい /京;10^16/", dictId: "") else { XCTFail(); return } 22 | XCTAssertEqual(entry.candidates[0].word, "京") 23 | XCTAssertEqual(entry.candidates[0].annotation?.text, "10^16") 24 | XCTAssertEqual(Entry(line: "あ /亜;*個人注釈/", dictId: "")?.candidates.first?.annotation?.text, "個人注釈") 25 | } 26 | 27 | func testOkuriBlock() throws { 28 | XCTAssertEqual(Entry(line: "おおk /大/多/[く/多/]/[き/大/]/", dictId: "")?.candidates.map { $0.word }, ["大", "多", "多", "大"]) 29 | XCTAssertEqual(Entry(line: "いt /[った/行/言/]/", dictId: "")?.candidates.map { $0.word }, ["行", "言"]) 30 | XCTAssertEqual(Entry(line: "いt /[った/行/]/[った/言/]/", dictId: "")?.candidates.map { $0.okuri }, ["った", "った"]) 31 | // 変換候補にスラッシュを含まない場合は送りありブロックではないと解釈する 32 | XCTAssertEqual(Entry(line: "ぶろっく /[ぶろっく]/", dictId: "")?.candidates.map { $0.word }, ["[ぶろっく]"]) 33 | } 34 | 35 | func testSpecialCase() { 36 | var entry = Entry(line: "から //", dictId: "") 37 | XCTAssertEqual(entry?.yomi, "から") 38 | XCTAssertEqual(entry?.candidates, []) 39 | entry = Entry(line: "から /空//殻/", dictId: "") 40 | XCTAssertEqual(entry?.yomi, "から") 41 | XCTAssertEqual(entry?.candidates, [Word("空"), Word("殻")]) 42 | entry = Entry(line: "から /;/", dictId: "") 43 | XCTAssertEqual(entry?.yomi, "から") 44 | XCTAssertEqual(entry?.candidates, []) 45 | entry = Entry(line: "から /;注釈/", dictId: "") 46 | XCTAssertEqual(entry?.yomi, "から") 47 | XCTAssertEqual(entry?.candidates, []) 48 | entry = Entry(line: "かっこ /[/", dictId: "") 49 | XCTAssertEqual(entry?.yomi, "かっこ") 50 | XCTAssertEqual(entry?.candidates, [Word("[")]) 51 | // 空の変換候補だけスキップ 52 | entry = Entry(line: "い /胃//意/", dictId: "") 53 | XCTAssertEqual(entry?.candidates, [Word("胃"), Word("意")]) 54 | entry = Entry(line: "い /胃/意//", dictId: "") 55 | XCTAssertEqual(entry?.candidates, [Word("胃"), Word("意")]) 56 | // 送り仮名ブロックの変換候補が空 57 | entry = Entry(line: "いt /[った//]/", dictId: "") 58 | XCTAssertEqual(entry?.candidates, []) 59 | // 送り仮名ブロックが閉じていない 60 | entry = Entry(line: "いt /[った/行/", dictId: "") 61 | XCTAssertEqual(entry?.candidates, [Word("[った"), Word("行")]) 62 | // 読みにある「う゛」は「ゔ」として扱う 63 | entry = Entry(line: "しう゛ぁ /湿婆/", dictId: "") 64 | XCTAssertEqual(entry?.yomi, "しゔぁ") 65 | } 66 | 67 | func testInvalidLine() { 68 | XCTAssertNil(Entry(line: "", dictId: "")) 69 | XCTAssertNil(Entry(line: ";こめんと /コメント/", dictId: "")) 70 | XCTAssertNil(Entry(line: "い/胃/", dictId: ""), "読みと変換候補の間にスペースがない") 71 | XCTAssertNil(Entry(line: "い /胃/", dictId: ""), "読みと変換候補の間にスペースが2つある") 72 | XCTAssertNil(Entry(line: "い /胃/意", dictId: ""), "末尾がスラッシュで終わらない") 73 | XCTAssertNil(Entry(line: "いt /[った/行]/", dictId: ""), "送り仮名ブロックの変換候補の末尾にスラッシュがない") 74 | } 75 | 76 | func testSerialize() { 77 | [ 78 | "あg /挙/揚/上/", 79 | "あ /亜;注釈/", 80 | "おおk /大/多/[く/多/]/[き/大/]/", 81 | "いt /[った/行/言/]/入/", 82 | "ふくm /含/[め/含/]/[む/含/]/[ま/含/]/[み/含/]/[も/含/]/", 83 | "すらっしゅ /(concat \"a\\057b\")/", 84 | "せみころん /(concat \"a\\073b\")/", 85 | "すらっしゅとせみころんがふくすう /(concat \"a\\073b\\057c\\073d\\057\\073e\")/", 86 | ].forEach { line in 87 | XCTAssertEqual(Entry(line: line, dictId: "")?.serialize(), line) 88 | } 89 | // シリアライズ時に送り仮名ブロックは同じ送り仮名でまとめられる 90 | XCTAssertEqual( 91 | Entry(line: "いt /[った/行/]/入/[った/言/]/", dictId: "")?.serialize(), 92 | "いt /[った/行/言/]/入/") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /macSKKTests/FileDictTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | import Combine 6 | 7 | @testable import macSKK 8 | 9 | final class FileDictTests: XCTestCase { 10 | let fileURL = Bundle(for: FileDictTests.self).url(forResource: "empty", withExtension: "txt")! 11 | var cancellables: Set = [] 12 | 13 | func testLoadContainsBom() throws { 14 | let fileURL = Bundle(for: Self.self).url(forResource: "utf8-bom", withExtension: "txt")! 15 | let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) 16 | XCTAssertEqual(dict.dict.entries, ["ゆにこーど": [Word("ユニコード")]]) 17 | } 18 | 19 | func testLoadJson() throws { 20 | let expectation = XCTestExpectation() 21 | NotificationCenter.default.publisher(for: notificationNameDictLoad).sink { notification in 22 | if let loadEvent = notification.object as? DictLoadEvent { 23 | if case .loaded(let loadCount, let failureCount) = loadEvent.status { 24 | if loadCount == 3 && failureCount == 0 { 25 | expectation.fulfill() 26 | } 27 | } 28 | } 29 | }.store(in: &cancellables) 30 | let fileURL = Bundle(for: Self.self).url(forResource: "SKK-JISYO.test", withExtension: "json")! 31 | let dict = try FileDict(contentsOf: fileURL, type: .json, readonly: true) 32 | XCTAssertEqual(dict.dict.refer("い", option: nil).map({ $0.word }).sorted(), ["伊", "胃"]) 33 | XCTAssertEqual(dict.dict.refer("あr", option: nil).map({ $0.word }).sorted(), ["在;注釈として解釈されない", "有"]) 34 | wait(for: [expectation], timeout: 1.0) 35 | } 36 | 37 | func testLoadJsonBroken() throws { 38 | let expectation = XCTestExpectation() 39 | NotificationCenter.default.publisher(for: notificationNameDictLoad).sink { notification in 40 | if let loadEvent = notification.object as? DictLoadEvent { 41 | if case .fail = loadEvent.status { 42 | expectation.fulfill() 43 | } 44 | } 45 | }.store(in: &cancellables) 46 | let fileURL = Bundle(for: Self.self).url(forResource: "SKK-JISYO.broken", withExtension: "json")! 47 | _ = try FileDict(contentsOf: fileURL, type: .json, readonly: true) 48 | wait(for: [expectation], timeout: 1.0) 49 | } 50 | 51 | func testAdd() throws { 52 | let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) 53 | XCTAssertEqual(dict.entryCount, 0) 54 | let word = Word("井") 55 | XCTAssertFalse(dict.hasUnsavedChanges) 56 | dict.add(yomi: "い", word: word) 57 | XCTAssertEqual(dict.refer("い", option: nil), [word]) 58 | XCTAssertTrue(dict.hasUnsavedChanges) 59 | } 60 | 61 | func testDelete() throws { 62 | let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: true) 63 | dict.setEntries(["あr": [Word("有"), Word("在")]], readonly: true) 64 | XCTAssertFalse(dict.delete(yomi: "あr", word: "或")) 65 | XCTAssertFalse(dict.hasUnsavedChanges) 66 | XCTAssertTrue(dict.delete(yomi: "あr", word: "在")) 67 | XCTAssertTrue(dict.hasUnsavedChanges) 68 | } 69 | 70 | func testSerialize() throws { 71 | let dict = try FileDict(contentsOf: fileURL, type: .traditional(.utf8), readonly: false) 72 | XCTAssertEqual(dict.serialize(), 73 | [FileDict.headers[0], FileDict.okuriAriHeader, FileDict.okuriNashiHeader, ""].joined(separator: "\n")) 74 | dict.add(yomi: "あ", word: Word("亜", annotation: Annotation(dictId: "testDict", text: "亜の注釈"))) 75 | dict.add(yomi: "あ", word: Word("阿", annotation: Annotation(dictId: "testDict", text: "阿の注釈"))) 76 | dict.add(yomi: "あr", word: Word("有", annotation: Annotation(dictId: "testDict", text: "有の注釈"))) 77 | dict.add(yomi: "あr", word: Word("在", annotation: Annotation(dictId: "testDict", text: "在の注釈"))) 78 | var expected = [ 79 | FileDict.headers[0], 80 | FileDict.okuriAriHeader, 81 | "あr /在;在の注釈/有;有の注釈/", 82 | FileDict.okuriNashiHeader, 83 | "あ /阿;阿の注釈/亜;亜の注釈/", 84 | "", 85 | ].joined(separator: "\n") 86 | XCTAssertEqual(dict.serialize(), expected) 87 | // 追加したエントリはシリアライズ時は先頭に付く 88 | dict.add(yomi: "い", word: Word("伊")) 89 | dict.add(yomi: "いr", word: Word("射")) 90 | expected = [ 91 | FileDict.headers[0], 92 | FileDict.okuriAriHeader, 93 | "いr /射/", 94 | "あr /在;在の注釈/有;有の注釈/", 95 | FileDict.okuriNashiHeader, 96 | "い /伊/", 97 | "あ /阿;阿の注釈/亜;亜の注釈/", 98 | "", 99 | ].joined(separator: "\n") 100 | XCTAssertEqual(dict.serialize(), expected) 101 | // 追加更新した場合は順序を変更する。削除更新した場合は順序を変更しない 102 | XCTAssertTrue(dict.delete(yomi: "あ", word: "亜")) 103 | dict.add(yomi: "あr", word: Word("或")) 104 | expected = [ 105 | FileDict.headers[0], 106 | FileDict.okuriAriHeader, 107 | "あr /或/在;在の注釈/有;有の注釈/", 108 | "いr /射/", 109 | FileDict.okuriNashiHeader, 110 | "い /伊/", 111 | "あ /阿;阿の注釈/", 112 | "", 113 | ].joined(separator: "\n") 114 | XCTAssertEqual(dict.serialize(), expected) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /macSKKTests/KeyBindingSetTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class KeyBindingSetTests: XCTestCase { 9 | func testInit() { 10 | let set = KeyBindingSet(id: "test", values: [ 11 | KeyBinding(.toggleKana, [.init(key: .character("q"), modifierFlags: [])]), 12 | KeyBinding(.japanese, [.init(key: .character("q"), modifierFlags: .shift)]), 13 | KeyBinding(.hiragana, [.init(key: .character("j"), modifierFlags: .control)]), 14 | KeyBinding(.direct, [.init(key: .character("l"), modifierFlags: [])]), 15 | KeyBinding(.unregister, [.init(key: .character("x"), modifierFlags: .shift)]), 16 | KeyBinding(.enter, [.init(key: .code(0x24), modifierFlags: [])]), 17 | KeyBinding(.left, [.init(key: .code(0x7b), modifierFlags: .function)]), 18 | KeyBinding(.left, [.init(key: .character("b"), modifierFlags: .control)]), 19 | ]) 20 | // まず修飾キー以外のキーの順でソートして、同じキーのときは修飾キーが多い方が前に来るようにソートされる 21 | // キーの順のソートはcodeが前、characterが後で、同じcodeやcharacterなら小さい方が前に来るようにソートされる 22 | XCTAssertEqual(set.sorted.map { $0.1 }, [.enter, .left, .left, .hiragana, .direct, .japanese, .toggleKana, .unregister]) 23 | } 24 | 25 | func testUpdate() { 26 | let set = KeyBindingSet(id: "test", values: [ 27 | KeyBinding(.toggleKana, [.init(key: .character("q"), modifierFlags: [])]), 28 | ]) 29 | var updated = set.update(for: .japanese, inputs: [.init(key: .character("q"), modifierFlags: .shift)]) 30 | // Shift-QのほうがQより前にくる 31 | XCTAssertEqual(updated.sorted.map { $0.1 }, [.japanese, .toggleKana]) 32 | updated = updated.update(for: .toggleKana, inputs: [.init(key: .character("a"), modifierFlags: [])]) 33 | XCTAssertEqual(updated.sorted.map { $0.1 }, [.toggleKana, .japanese]) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /macSKKTests/KeyTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class KeyTests: XCTestCase { 9 | func testEncodeAndDecode() { 10 | let key1: Key = .character("q") 11 | XCTAssertEqual(key1, Key(rawValue: key1.encode())) 12 | let key2: Key = .code(0x66) 13 | XCTAssertEqual(key2, Key(rawValue: key2.encode())) 14 | let key3: Key = .character("Q") 15 | XCTAssertNil(Key(rawValue: key3.encode())) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /macSKKTests/NumberEntryTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class NumberEntryTests: XCTestCase { 9 | func testNumberYomi() { 10 | XCTAssertNil(NumberYomi("")) 11 | XCTAssertNil(NumberYomi("あい"), "整数が入ってないとnil") 12 | XCTAssertEqual(NumberYomi("あい1うえ")?.elements, [.other("あい"), .number(1), .other("うえ")]) 13 | XCTAssertEqual(NumberYomi("あい-100")?.elements, [.other("あい-"), .number(100)]) 14 | XCTAssertEqual(NumberYomi("123456789あい")?.elements, [.number(123456789), .other("あい")]) 15 | XCTAssertEqual(NumberYomi("あ1い2う3")?.elements, [.other("あ"), .number(1), .other("い"), .number(2), .other("う"), .number(3)]) 16 | XCTAssertEqual(NumberYomi("18446744073709551615")?.elements, [.number(18446744073709551615)], "UInt64の最大値") 17 | XCTAssertNil(NumberYomi("18446744073709551616")) 18 | } 19 | 20 | func testToMidashiString() { 21 | XCTAssertEqual(NumberYomi("あい123う")?.toMidashiString(), "あい#う") 22 | XCTAssertEqual(NumberYomi("0123456789あい")?.toMidashiString(), "#あい") 23 | XCTAssertEqual(NumberYomi("あ1い2う3")?.toMidashiString(), "あ#い#う#") 24 | } 25 | 26 | func testNumberYomiNumberElements() { 27 | XCTAssertEqual(NumberYomi("あい123う")?.numberElements, [123]) 28 | XCTAssertEqual(NumberYomi("0123456789あい")?.numberElements, [123456789]) 29 | XCTAssertEqual(NumberYomi("1あ2い3う4")?.numberElements, [1, 2, 3, 4]) 30 | } 31 | 32 | func testNumberCandidate() throws { 33 | XCTAssertEqual(try NumberCandidate(yomi: "").elements, []) 34 | XCTAssertEqual(try NumberCandidate(yomi: "#0").elements, [.number(0)]) 35 | XCTAssertEqual(try NumberCandidate(yomi: "あ#1い#2").elements, [.other("あ"), .number(1), .other("い"), .number(2)]) 36 | XCTAssertEqual(try NumberCandidate(yomi: "#3うえお##4か#5#6き#8く#9け").elements, [.number(3), 37 | .other("うえお#"), 38 | .number(4), 39 | .other("か"), 40 | .number(5), 41 | .other("#6き"), 42 | .number(8), 43 | .other("く"), 44 | .number(9), 45 | .other("け")]) 46 | } 47 | 48 | func testNumberCandidateToString() throws { 49 | XCTAssertEqual(try NumberCandidate(yomi: "第#0回").toString(yomi: NumberYomi("だい100かい")!), "第100回") 50 | XCTAssertEqual(try NumberCandidate(yomi: "#1位").toString(yomi: NumberYomi("100い")!), "100位") 51 | XCTAssertEqual(try NumberCandidate(yomi: "#2").toString(yomi: NumberYomi("2309")!), "二三〇九") 52 | XCTAssertEqual(try NumberCandidate(yomi: "#3").toString(yomi: NumberYomi("123456789")!), "一億二千三百四十五万六千七百八十九") 53 | XCTAssertEqual(try NumberCandidate(yomi: "#0").toString(yomi: NumberYomi("9223372036854775807")!), "9223372036854775807") 54 | XCTAssertEqual(try NumberCandidate(yomi: "#1").toString(yomi: NumberYomi("9223372036854775807")!), "9223372036854775807") 55 | XCTAssertEqual(try NumberCandidate(yomi: "#2").toString(yomi: NumberYomi("9223372036854775807")!), "九二二三三七二〇三六八五四七七五八〇七") 56 | XCTAssertEqual(try NumberCandidate(yomi: "#8").toString(yomi: NumberYomi("9223372036854775807")!), "9,223,372,036,854,775,807") 57 | XCTAssertEqual(try NumberCandidate(yomi: "#9金").toString(yomi: NumberYomi("34きん")!), "3四金") 58 | XCTAssertEqual(try NumberCandidate(yomi: "#9").toString(yomi: NumberYomi("3")!), nil, "数値は2桁で11 - 99である必要がある") 59 | XCTAssertEqual(try NumberCandidate(yomi: "#9").toString(yomi: NumberYomi("111")!), nil) 60 | XCTAssertEqual(try NumberCandidate(yomi: "#9").toString(yomi: NumberYomi("50")!), nil) 61 | XCTAssertEqual(try NumberCandidate(yomi: "#0").toString(yomi: NumberYomi("1,2")!), nil, "数値の数が合わないとnil") 62 | XCTAssertEqual(try NumberCandidate(yomi: "#0/#0").toString(yomi: NumberYomi("あ1い")!), nil, "数値の数が合わないとnil") 63 | XCTAssertEqual(try NumberCandidate(yomi: "#0").toString(yomi: NumberYomi("だい100かい")!), "100") 64 | XCTAssertEqual(try NumberCandidate(yomi: "#0,#1").toString(yomi: NumberYomi("100と200と")!), "100,200") 65 | // SKK-JISYO.Lには数値変換らしきエントリで候補に "#数字" を含まないものがある。 66 | // 例えば "だい#" という見出しに "第" だけが登録されている。数値を切り捨ててほしいのかな…? 67 | XCTAssertEqual(try NumberCandidate(yomi: "第").toString(yomi: NumberYomi("だい2")!), nil) 68 | } 69 | 70 | func testNumberCandidateToStringTodo() throws { 71 | XCTExpectFailure("未実装") 72 | XCTAssertEqual(try NumberCandidate(yomi: "#5").toString(yomi: NumberYomi("1995")!), "壱阡九百九拾伍") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /macSKKTests/PunctuationTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import XCTest 4 | 5 | @testable import macSKK 6 | 7 | final class PunctuationTests: XCTestCase { 8 | func testInit() { 9 | guard var punctuation = Punctuation(rawValue: 0) else { XCTFail(); return } // default, default 10 | XCTAssertEqual(punctuation.comma, .default) 11 | XCTAssertEqual(punctuation.period, .default) 12 | punctuation = Punctuation(comma: .comma, period: .maru) 13 | XCTAssertEqual(punctuation.comma, .comma) 14 | XCTAssertEqual(punctuation.period, .maru) 15 | XCTAssertEqual(punctuation.rawValue, 256 | 2) 16 | let punctuation2 = Punctuation(rawValue: punctuation.rawValue) 17 | XCTAssertEqual(punctuation2?.comma, .comma) 18 | XCTAssertEqual(punctuation2?.period, .maru) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /macSKKTests/ReleaseVersionTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class ReleaseVersionTests: XCTestCase { 9 | func testComparable() { 10 | XCTAssertTrue(ver(1, 0, 0) > ver(0, 9, 9)) 11 | XCTAssertTrue(ver(10, 0, 0) > ver(9, 9, 9)) 12 | XCTAssertTrue(ver(0, 10, 0) > ver(0, 9, 9)) 13 | XCTAssertTrue(ver(0, 0, 10) > ver(0, 0, 9)) 14 | } 15 | 16 | func testInit() { 17 | XCTAssertEqual(try? ReleaseVersion(string: "1.2.30"), ver(1, 2, 30)) 18 | XCTAssertNil(try? ReleaseVersion(string: "v1.0.0"), "数字3つ以外つけてはいけない") 19 | XCTAssertNil(try? ReleaseVersion(string: "2.0"), "数字は3つないといけない") 20 | XCTAssertNil(try? ReleaseVersion(string: "0.f.0"), "数字は10進数じゃないといけない") 21 | XCTAssertNil(try? ReleaseVersion(string: "0.1.0-beta1"), "ベータバージョンのような形式は受理しない") 22 | } 23 | 24 | private func ver(_ major: Int = 0, _ minor: Int = 0, _ patch: Int = 0) -> ReleaseVersion { 25 | return ReleaseVersion(major: major, minor: minor, patch: patch) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /macSKKTests/SKKServDictTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | @testable import macSKK 6 | 7 | final class SKKServDictTests: XCTestCase { 8 | struct MockedSKKServService: SKKServServiceProtocol { 9 | let response: String 10 | 11 | func refer(yomi: String, destination: SKKServDestination, timeout: TimeInterval) throws -> String { 12 | return response 13 | } 14 | 15 | func disconnect() throws {} 16 | } 17 | 18 | let destination = SKKServDestination(host: "localhost", port: 1178, encoding: .japaneseEUC) 19 | 20 | func testRefer() async throws { 21 | let service = MockedSKKServService(response: "1/変換/返還/") 22 | let dict = SKKServDict(destination: destination, service: service) 23 | XCTAssertEqual(dict.refer("へんかん", option: nil).map { $0.word }, ["変換", "返還"]) 24 | } 25 | 26 | func testReferNotFound() async throws { 27 | let service = MockedSKKServService(response: "4へんかん") 28 | let dict = SKKServDict(destination: destination, service: service) 29 | XCTAssertEqual(dict.refer("へんかん", option: nil).map { $0.word }, []) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /macSKKTests/String+TransformTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class StringTransformTests: XCTestCase { 9 | func testToZenkaku() throws { 10 | XCTAssertEqual("".toZenkaku(), "") 11 | XCTAssertEqual("a".toZenkaku(), "a") 12 | XCTAssertEqual("A".toZenkaku(), "A") 13 | XCTAssertEqual(";".toZenkaku(), ";") 14 | XCTAssertEqual(" ".toZenkaku(), " ") 15 | XCTAssertEqual("アッ".toZenkaku(), "アッ") 16 | XCTAssertEqual("グヴェ".toZenkaku(), "グヴェ") 17 | XCTAssertEqual("、。ー;".toZenkaku(), "、。ー;") 18 | } 19 | 20 | func testToHankaku() throws { 21 | XCTAssertEqual("".toHankaku(), "") 22 | XCTAssertEqual("a".toHankaku(), "a") 23 | XCTAssertEqual("A".toHankaku(), "A") 24 | XCTAssertEqual(";".toHankaku(), ";") 25 | XCTAssertEqual(" ".toHankaku(), " ") 26 | XCTAssertEqual("アッ".toHankaku(), "アッ") 27 | XCTAssertEqual("グヴェ".toHankaku(), "グヴェ") 28 | XCTAssertEqual("あいう123!@#".toHankaku(), "あいう123!@#") 29 | XCTAssertEqual("、。ー;".toHankaku(), "、。ー;") 30 | 31 | } 32 | 33 | func testToKatakana() throws { 34 | XCTAssertEqual("".toKatakana(), "") 35 | XCTAssertEqual("あっ".toKatakana(), "アッ") 36 | XCTAssertEqual("ぐう゛ぇ".toKatakana(), "グヴェ") 37 | XCTAssertEqual("ぐゔぇ".toKatakana(), "グヴェ") 38 | } 39 | 40 | func testIsAlphabet() { 41 | XCTAssertTrue("".isAlphabet, "空文字列はtrue") 42 | XCTAssertTrue("abcdefghijklmnopqrstuvwxyz".isAlphabet) 43 | XCTAssertTrue("ABCDEFGHIJKLMNOPQRSTUVWXYZ".isAlphabet) 44 | XCTAssertFalse("1".isAlphabet) 45 | XCTAssertFalse("å".isAlphabet, "Option+A") 46 | XCTAssertFalse("!".isAlphabet) 47 | XCTAssertFalse("あ".isAlphabet) 48 | } 49 | 50 | func testIsHiragana() { 51 | XCTAssertTrue("".isHiragana, "空文字列はtrue") 52 | XCTAssertTrue("ひらけごま".isHiragana) 53 | XCTAssertFalse("アイウエオ".isHiragana) 54 | } 55 | 56 | func testIsOkuriAri() { 57 | XCTAssertFalse("".isOkuriAri, "空文字列はfalse") 58 | XCTAssertTrue("あr".isOkuriAri) 59 | XCTAssertFalse("あいうえお".isOkuriAri) 60 | XCTAssertFalse("い58".isOkuriAri) 61 | XCTAssertFalse("skk".isOkuriAri, "Abbrevの見出し") 62 | XCTAssertFalse("b".isOkuriAri) 63 | XCTAssertFalse("ん".isOkuriAri) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /macSKKTests/UpdateCheckerTests.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import XCTest 5 | 6 | @testable import macSKK 7 | 8 | final class UpdateCheckerTests: XCTestCase { 9 | func testDecode() throws { 10 | let fileURL = Bundle(for: Self.self).url(forResource: "release", withExtension: "json")! 11 | let data = try Data(NSData(contentsOf: fileURL)) 12 | let decoder = JSONDecoder() 13 | decoder.dateDecodingStrategy = .iso8601 14 | let release = try decoder.decode(Release.self, from: data) 15 | XCTAssertEqual(release.version, ReleaseVersion(major: 1, minor: 11, patch: 0)) 16 | XCTAssertEqual(release.updated.ISO8601Format(), "2025-02-23T11:36:20Z") 17 | XCTAssertEqual(release.url.absoluteString, "https://github.com/mtgto/macSKK/releases/tag/1.11.0") 18 | XCTAssertEqual(release.content, "- 補完候補をSKKServから取得するテスト機能を設定画面に追加 (#309)\r\n- abbrevモードでTab補完すると前のモードに戻れなくなるバグを修正 (#312)\r\n- Spaceにスペースキー以外を割り当てているときnormalモードでは入力されたキーの入力を優先する (#310)\r\n- 起動時に設定変更したようなログが出ていたのを修正 (#313)\r\n- ひらがなモードで文字未入力時はPageDnなどの特殊キーを無視する (#314)\r\n- 選択文字列から変換候補を逆引きして読みとして変換を再度開始 (再変換) (#294)") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /macSKKTests/UserDict+Utilities.swift: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 mtgto 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | @testable import macSKK 5 | 6 | extension UserDict { 7 | func setEntries(_ entries: [String: [Word]]) { 8 | if let dict = userDict as? FileDict { 9 | dict.setEntries(entries, readonly: true) 10 | } 11 | } 12 | 13 | func entries() -> [String: [Word]]? { 14 | if let dict = userDict as? FileDict { 15 | return dict.dict.entries 16 | } 17 | return nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /macSKKTests/fixture/SKK-JISYO.broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "copyright": "macSKK Test", 4 | "license": "", 5 | "okuri_ari": { 6 | "あr": [ 7 | "有", 8 | "在" 9 | ], 10 | "あk": [ 11 | "開", 12 | "空" 13 | ] 14 | }, 15 | "okuri_nasi": { 16 | "い": [ 17 | "胃", 18 | "伊" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /macSKKTests/fixture/SKK-JISYO.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "copyright": "macSKK Test", 4 | "license": "", 5 | "okuri_ari": { 6 | "あr": [ 7 | "有", 8 | "在;注釈として解釈されない" 9 | ], 10 | "あk": [ 11 | "開", 12 | "空" 13 | ] 14 | }, 15 | "okuri_nasi": { 16 | "い": [ 17 | "胃", 18 | "伊" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /macSKKTests/fixture/SKK-JISYO.test.utf8: -------------------------------------------------------------------------------- 1 | じてん /辞典/事典/字典/ 2 | -------------------------------------------------------------------------------- /macSKKTests/fixture/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKKTests/fixture/empty.txt -------------------------------------------------------------------------------- /macSKKTests/fixture/euc-jis-2004.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtgto/macSKK/b3093917fd61a64b54b29dd295f27091d815bf03/macSKKTests/fixture/euc-jis-2004.txt -------------------------------------------------------------------------------- /macSKKTests/fixture/kana-rule-for-test.conf: -------------------------------------------------------------------------------- 1 | # RomajiTestsの読み込み用のテストデータ 2 | a,あ 3 | i,い,イ 4 | u,う,ウ,ウ 5 | ka,か,カ,カ 6 | kk,っ,ッ,ッ,k 7 | ga,が 8 | za,ざ 9 | zi,じ 10 | ji,じ 11 | na,な 12 | nya,にゃ 13 | n,ん 14 | nn,ん 15 | ,,、 16 | z,,‥ 17 | x,,,だぶるかんま 18 | b;,びーせみころん 19 | ca,か 20 | +,; 21 | :,; 22 | ♯,しゃーぷ 23 | -------------------------------------------------------------------------------- /macSKKTests/fixture/utf8-bom.txt: -------------------------------------------------------------------------------- 1 | ゆにこーど /ユニコード/ 2 | -------------------------------------------------------------------------------- /script/app.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BundleHasStrictIdentifier 7 | 8 | BundleIsRelocatable 9 | 10 | BundleIsVersionChecked 11 | 12 | BundleOverwriteAction 13 | upgrade 14 | RootRelativeBundlePath 15 | Library/Input Methods/macSKK.app 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /script/dict.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /script/distribution.xml.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %TITLE% 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | app.pkg 17 | 18 | 19 | dict.pkg 20 | 21 | 22 | -------------------------------------------------------------------------------- /script/export-options.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | compileBitcode 6 | 7 | method 8 | developer-id 9 | signingStyle 10 | automatic 11 | 12 | 13 | -------------------------------------------------------------------------------- /script/scripts/postinstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | asns=$(/usr/bin/lsappinfo find bundleID=net.mtgto.inputmethod.macSKK) 3 | if [[ "$asns" = ASN* ]]; then 4 | osascript -e 'tell application id "net.mtgto.inputmethod.macSKK" to quit' 5 | fi 6 | -------------------------------------------------------------------------------- /script/welcome.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg932\cocoartf2761 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;\f2\fnil\fcharset128 HiraginoSans-W3; 3 | \f3\fnil\fcharset128 HiraginoSans-W6;\f4\fmodern\fcharset0 Courier;} 4 | {\colortbl;\red255\green255\blue255;} 5 | {\*\expandedcolortbl;;} 6 | {\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}} 7 | {\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} 8 | \paperw11900\paperh16840\margl1440\margr1440\vieww17200\viewh7880\viewkind0 9 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 10 | 11 | \f0\b\fs24 \cf0 macSKK 12 | \f1\b0 \ 13 | \ 14 | macSKK 15 | \f2 \'82\'cd 16 | \f1 macOS 17 | \f2 \'97\'70\'82\'cc 18 | \f1 SKK 19 | \f2 \'95\'fb\'8e\'ae\'82\'cc\'93\'fa\'96\'7b\'8c\'ea\'93\'fc\'97\'cd\'83\'81\'83\'5c\'83\'62\'83\'68\'82\'c5\'82\'b7\'81\'42 20 | \f1 \ 21 | \ 22 | macOS 23 | \f2 \'97\'70\'82\'cc 24 | \f1 SKK 25 | \f2 \'95\'fb\'8e\'ae\'82\'cc\'93\'fa\'96\'7b\'8c\'ea\'93\'fc\'97\'cd\'83\'81\'83\'5c\'83\'62\'83\'68\'82\'c9\'82\'cd\'82\'b7\'82\'c5\'82\'c9 26 | \f1 AquaSKK 27 | \f2 \'82\'aa\'82\'a0\'82\'e8\'82\'dc\'82\'b7\'82\'aa\'81\'41\'82\'a2\'82\'ad\'82\'c2\'82\'a9\'93\'c6\'8e\'a9\'82\'cc\'8b\'40\'94\'5c\'82\'f0\'8d\'ec\'82\'e8\'82\'bd\'82\'a2\'82\'c6\'8e\'76\'82\'a2\'90\'56\'82\'bd\'82\'c9\'8a\'4a\'94\'ad\'82\'b5\'82\'c4\'82\'a2\'82\'dc\'82\'b7\'81\'42 28 | \f1 \ 29 | \ 30 | macSKK 31 | \f2 \'82\'f0\'8e\'67\'97\'70\'82\'b7\'82\'e9\'82\'c9\'82\'cd 32 | \f1 macOS 13.3 33 | \f2 \'88\'c8\'8d\'7e\'82\'aa\'95\'4b\'97\'76\'82\'c5\'82\'b7\'81\'42 34 | \f1 Universal Binary 35 | \f2 \'82\'c5\'83\'72\'83\'8b\'83\'68\'82\'b5\'82\'c4\'82\'a2\'82\'dc\'82\'b7\'82\'aa\'81\'41\'93\'ae\'8d\'ec\'8a\'6d\'94\'46\'82\'cd 36 | \f1 Apple Silicon 37 | \f2 \'8a\'c2\'8b\'ab\'82\'c5\'82\'cc\'82\'dd\'8d\'73\'82\'c1\'82\'c4\'82\'a2\'82\'dc\'82\'b7\'81\'42 38 | \f1 \ 39 | \ 40 | macSKK 41 | \f2 \'82\'cd 42 | \f1 GNU 43 | \f2 \'88\'ea\'94\'ca\'8c\'f6\'8f\'4f\'83\'89\'83\'43\'83\'5a\'83\'93\'83\'58 44 | \f1 v3 45 | \f2 \'82\'dc\'82\'bd\'82\'cd\'82\'bb\'82\'ea\'88\'c8\'8d\'7e\'82\'cc\'83\'6f\'81\'5b\'83\'57\'83\'87\'83\'93\'82\'cc\'8f\'f0\'8d\'80\'82\'cc\'8c\'b3\'82\'c5\'94\'7a\'95\'7a\'82\'b3\'82\'ea\'82\'e9\'83\'74\'83\'8a\'81\'5b\'81\'45\'83\'5c\'83\'74\'83\'67\'83\'45\'83\'46\'83\'41\'82\'c5\'82\'b7\'81\'42\'8a\'ae\'91\'53\'82\'c8\'83\'5c\'81\'5b\'83\'58\'83\'52\'81\'5b\'83\'68\'82\'cc\'83\'52\'83\'73\'81\'5b\'82\'cd 46 | \f1 {\field{\*\fldinst{HYPERLINK "https://github.com/mtgto/macSKK"}}{\fldrslt https://github.com/mtgto/macSKK}} 47 | \f2 \'82\'a9\'82\'e7\'8e\'e6\'93\'be\'89\'c2\'94\'5c\'82\'c5\'82\'b7\'81\'42 48 | \f1 \ 49 | \ 50 | 51 | \f3\b \'83\'43\'83\'93\'83\'58\'83\'67\'81\'5b\'83\'8b 52 | \f1\b0 \ 53 | \ 54 | 55 | \f2 \'83\'43\'83\'93\'83\'58\'83\'67\'81\'5b\'83\'8b\'8a\'ae\'97\'b9\'8c\'e3\'82\'c9\'83\'56\'83\'58\'83\'65\'83\'80\'90\'dd\'92\'e8\'81\'a8\'83\'4c\'81\'5b\'83\'7b\'81\'5b\'83\'68\'81\'a8\'93\'fc\'97\'cd\'83\'5c\'81\'5b\'83\'58\'82\'a9\'82\'e7\'81\'75\'82\'d0\'82\'e7\'82\'aa\'82\'c8\'81\'76(\'83\'41\'83\'43\'83\'52\'83\'93\'82\'cd\'81\'75\'81\'a5\'82\'a0\'81\'76)\'82\'c6\'81\'75 56 | \f1 ABC 57 | \f2 \'81\'76(\'83\'41\'83\'43\'83\'52\'83\'93\'82\'cd\'81\'75\'81\'a5A\'81\'76) \'82\'f0\'92\'c7\'89\'c1\'82\'b5\'82\'c4\'82\'ad\'82\'be\'82\'b3\'82\'a2\'81\'42\'83\'4a\'83\'5e\'83\'4a\'83\'69\'81\'41\'91\'53\'8a\'70\'89\'70\'90\'94\'81\'41\'94\'bc\'8a\'70\'83\'4a\'83\'69\'82\'cd\'92\'c7\'89\'c1\'82\'b5\'82\'c4\'82\'e0\'92\'c7\'89\'c1\'82\'b5\'82\'c8\'82\'ad\'82\'c4\'82\'e0\'96\'e2\'91\'e8\'82\'a0\'82\'e8\'82\'dc\'82\'b9\'82\'f1\'81\'42 58 | \f1 59 | \f2 \'82\'e0\'82\'b5\'83\'43\'83\'93\'83\'58\'83\'67\'81\'5b\'83\'8b\'92\'bc\'8c\'e3\'82\'c9\'95\'5c\'8e\'a6\'82\'b3\'82\'ea\'82\'c8\'82\'a9\'82\'c1\'82\'bd\'82\'e8\'81\'41\'82\'b3\'82\'b5\'82\'a9\'82\'a6\'82\'c4\'82\'e0\'94\'bd\'89\'66\'82\'b3\'82\'ea\'82\'c8\'82\'a2\'8f\'ea\'8d\'87\'82\'cd\'83\'8d\'83\'4f\'83\'41\'83\'45\'83\'67 60 | \f1 & 61 | \f2 \'83\'8d\'83\'4f\'83\'43\'83\'93\'82\'f0\'8e\'8e\'82\'b5\'82\'c4\'82\'dd\'82\'c4\'82\'ad\'82\'be\'82\'b3\'82\'a2\'81\'42 62 | \f1 \ 63 | \ 64 | 65 | \f3\b \'83\'41\'83\'93\'83\'43\'83\'93\'83\'58\'83\'67\'81\'5b\'83\'8b 66 | \f1\b0 \ 67 | \ 68 | 69 | \f2 \'83\'43\'83\'93\'83\'58\'83\'67\'81\'5b\'83\'89\'82\'c9\'83\'41\'83\'93\'83\'43\'83\'93\'83\'58\'83\'67\'81\'5b\'83\'89\'82\'f0\'93\'af\'8d\'ab\'97\'5c\'92\'e8\'82\'c5\'82\'b7\'81\'42 70 | \f1 \ 71 | \ 72 | 73 | \f2 \'8e\'e8\'93\'ae\'82\'c5\'8d\'73\'82\'a4\'82\'c9\'82\'cd\'81\'41\'83\'56\'83\'58\'83\'65\'83\'80\'90\'dd\'92\'e8\'81\'a8\'83\'4c\'81\'5b\'83\'7b\'81\'5b\'83\'68\'81\'a8\'93\'fc\'97\'cd\'83\'5c\'81\'5b\'83\'58\'82\'a9\'82\'e7\'81\'75\'82\'d0\'82\'e7\'82\'aa\'82\'c8\'81\'76\'81\'75 74 | \f1 ABC 75 | \f2 \'81\'76\'82\'f0\'8d\'ed\'8f\'9c\'8c\'e3\'81\'41\'88\'c8\'89\'ba\'82\'cc\'83\'74\'83\'40\'83\'43\'83\'8b\'82\'f0\'8d\'ed\'8f\'9c\'82\'b5\'82\'c4\'82\'ad\'82\'be\'82\'b3\'82\'a2\'81\'42 76 | \f1 \ 77 | \ 78 | \pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\pardirnatural\partightenfactor0 79 | \ls1\ilvl0\cf0 {\listtext \uc0\u8226 } 80 | \f4\fs26 \expnd0\expndtw0\kerning0 81 | ~/Library/Input Methods/macSKK.app\ 82 | \ls1\ilvl0 83 | \f1\fs24 \kerning1\expnd0\expndtw0 {\listtext \uc0\u8226 } 84 | \f4\fs26 \expnd0\expndtw0\kerning0 85 | ~/Library/Containers/net.mtgto.inputmethod.macSKK\ 86 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 87 | 88 | \f1\fs24 \cf0 \kerning1\expnd0\expndtw0 \ 89 | } --------------------------------------------------------------------------------