├── .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 | }
--------------------------------------------------------------------------------