Skip to content

Commit d1b18c6

Browse files
authored
Merge pull request #20 from mad-p/feature/auto_bushu
学習関連機能
2 parents 7b6c4c5 + 0d9dce8 commit d1b18c6

25 files changed

Lines changed: 820 additions & 151 deletions

Claude.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,151 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
15
- ユーザーへは日本語で応答してください
6+
7+
## プロジェクト概要
8+
9+
MacTcodeは、macOS用のT-Code日本語入力メソッド(IME)です。T-Codeは2ストロークで日本語文字を入力する効率的な入力方式で、部首変換や交ぜ書き変換などの機能を提供します。
10+
11+
## ビルドとテスト
12+
13+
### 開発サイクル
14+
```bash
15+
make reload # デバッグビルド→インストール→入力メソッド再起動
16+
make releaseBuild # リリースビルド
17+
make test # テスト実行
18+
```
19+
20+
### ログの確認
21+
設定ファイルで`"logEnabled": true`を設定後、以下で確認:
22+
```bash
23+
log stream --predicate 'process == "MacTcode"'
24+
```
25+
26+
### 設定ファイルの場所
27+
```
28+
~/Library/Containers/jp.mad-p.inputmethod.MacTcode/Data/Library/Application Support/MacTcode/config.json
29+
```
30+
31+
## コアアーキテクチャ
32+
33+
### 三角関係: Mode - Controller - Client
34+
35+
**Mode** (入力状態):
36+
- `handle()`: 入力イベントを処理
37+
- 例: `TcodeMode`, `ZenkakuMode`, `MazegakiSelectionMode`
38+
39+
**Controller** (モード管理):
40+
- `TcodeInputController``IMKInputController`を継承
41+
- モードスタックで状態遷移を管理
42+
- `pushMode()`/`popMode()`で一時的なモード切替
43+
44+
**Client** (テキスト入出力):
45+
- `ContextClient`: カーソル周辺のテキスト取得(複雑なロジック)
46+
- `RecentTextClient`: クライアントが文脈を提供できない場合のフォールバック
47+
- 読み取得の順序: 選択範囲 → クライアント → ミラー
48+
49+
### キーマップシステム
50+
51+
入力フロー: `キーイベント → KeymapResolver → Command → Action`
52+
53+
- **KeymapResolver**: キーシーケンスを解決し、Commandに変換
54+
- **Command**: `.passthrough`, `.processed`, `.pending`, `.text(String)`, `.action(Action)`, `.keymap(Keymap)`
55+
- **Action**: 実際の処理(部首変換、交ぜ書き変換など)
56+
57+
### 変換エンジン
58+
59+
**部首変換 (Bushu)**:
60+
- `Bushu.swift`: tc-bushu.elアルゴリズムの再実装、自動学習データ管理
61+
- 2文字の組み合わせから1文字を合成
62+
- 部品単位の合成、引き算(部品の削除)をサポート
63+
- `autoDict`: 自動部首変換学習データ(受容された変換結果を記録)
64+
- `tryAutoBushu()`: 文字入力後に自動変換を試行
65+
66+
**交ぜ書き変換 (Mazegaki)**:
67+
- `MazegakiDict.swift`: 辞書ファイル読み込み、LRU学習データ管理
68+
- `Mazegaki.swift`: 読み取得と変換候補の検索
69+
- `MazegakiSelectionMode.swift`: 候補選択UI
70+
- `MazegakiHit.swift`: 変換候補、LRU学習優先の候補取得
71+
72+
### 学習機能とキャンセル期間
73+
74+
**PendingKakutei** (変換キャンセル機構):
75+
- 変換確定後、`cancelPeriod`秒間(デフォルト1.5秒)キャンセル可能
76+
- Delete、Control-g、Escapeキーでキャンセルして読みに戻せる
77+
- キャンセルされなかった変換は「受容」され、学習データに反映
78+
- `Controller`プロトコルの`pendingKakutei`プロパティで管理
79+
80+
**交ぜ書き候補LRU学習**:
81+
- 選択された候補を先頭に移動(LRU: Least Recently Used)
82+
- `MazegakiDict.lruDict`で学習データを管理(元辞書は不変)
83+
- `MazegakiHit.candidates()`はLRU辞書を優先的に参照
84+
- `mazegaki_user.dic`に自動保存(統計データと同じタイミング)
85+
- 設定: `mazegaki.lruEnabled`, `mazegaki.lruFile`
86+
87+
**自動部首変換**:
88+
- 手動部首変換で受容された結果を学習し、次回から自動的に変換
89+
- `Bushu.autoDict`で学習データを管理(キー: 合成元2文字、値: 合成結果1文字)
90+
- `TcodeMode.handle()`で文字入力後に`tryAutoBushu()`を実行
91+
- 順序厳密: "木林"で学習したものは"林木"では自動変換されない
92+
- 自動変換もキャンセル可能(受容時は学習データ更新なし)
93+
- `bushu_auto.dic`に自動保存(統計データと同じタイミング)
94+
- 設定: `bushu.autoEnabled`, `bushu.autoFile`
95+
96+
### 設定管理
97+
98+
`UserConfigs.shared`(シングルトン)が5つの設定カテゴリを管理:
99+
1. **MazegakiConfig**: 交ぜ書き変換設定、LRU学習設定
100+
2. **BushuConfig**: 部首変換設定、自動部首変換学習設定
101+
3. **KeyBindingsConfig**: キーバインド、基本文字配列(40x40)
102+
4. **UIConfig**: 候補選択キー、記号セット
103+
5. **SystemConfig**: 除外アプリ、ログ、統計同期間隔、キャンセル期間
104+
105+
設定変更は`UserConfigsDelegate`プロトコルで通知。
106+
107+
### 統計管理
108+
109+
`InputStats.shared`(シングルトン):
110+
- スレッドセーフ(DispatchQueueで排他制御)
111+
- 基本文字、部首変換、交ぜ書き変換、機能実行をカウント
112+
- 定期的に`tc-record.txt`に出力(デフォルト1200秒間隔)
113+
114+
## 重要なパターン
115+
116+
1. **プロトコル指向**: `Mode`, `Controller`, `Client`などで責務を明確化
117+
2. **シングルトン**: `UserConfigs.shared`, `InputStats.shared`, `MazegakiDict.i`, `Bushu.i`
118+
3. **モードスタック**: 候補選択などのサブモードを一時的にプッシュ
119+
4. **フォールバック**: クライアント → ミラーの順で読み取得、設定不正時はデフォルトで動作
120+
121+
## 主要ファイル
122+
123+
| ファイル | 役割 |
124+
|---------|------|
125+
| `AppDelegate.swift` | IMKServer初期化、シグナルハンドラ、統計保存 |
126+
| `TcodeInputController.swift` | 入力制御の中核、モードスタック管理 |
127+
| `TcodeMode.swift` | 基本T-Code入力モード |
128+
| `KeymapResolver.swift` | キーシーケンス解決エンジン |
129+
| `ContextClient.swift` | テキスト読み取りの複雑なロジック |
130+
| `UserConfigs.swift` | 設定管理システム |
131+
| `Bushu.swift` | 部首変換アルゴリズム |
132+
| `MazegakiDict.swift` | 交ぜ書き辞書、LRU学習データ |
133+
| `PendingKakutei.swift` | 変換キャンセル機構 |
134+
| `InputStats.swift` | 統計管理 |
135+
136+
## テスト
137+
138+
テストは`MacTcodeTests/`に配置。最大規模のテストは`ContextClientTests.swift`(19253行)。
139+
140+
```bash
141+
make test # すべてのテストを実行
142+
```
143+
144+
## 開発状況
145+
146+
完了した機能:
147+
- ✅ キャンセル期間機能(PendingKakutei)
148+
- ✅ 交ぜ書き候補LRU学習
149+
- ✅ 自動部首変換機能
150+
151+
学習機能の実装はすべて完了。詳細は`TODO.md`を参照。

ConfigParams.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,37 @@ MacTcodeは`config.json`形式の設定ファイルを使用してカスタマ
3232
"maxInflection": 4,
3333
"maxYomi": 10,
3434
"mazegakiYomiCharacters": "々ー\\p{Hiragana}\\p{Katakana}\\p{Han}",
35-
"dictionaryFile": "mazegaki.dic"
35+
"dictionaryFile": "mazegaki.dic",
36+
"lruEnabled": false,
37+
"lruFile": "mazegaki_user.dic"
3638
}
3739
```
3840

3941
- **`maxInflection`**: 活用部分の最大文字数(1-10)
4042
- **`maxYomi`**: 読みの最大文字数(1-50)
4143
- **`mazegakiYomiCharacters`**: 交ぜ書き変換で読み部分に含める文字の正規表現文字クラス記法
4244
- **`dictionaryFile`**: 交ぜ書き変換辞書のファイル名
45+
- **`lruEnabled`**: LRU(Least Recently Used)学習機能の有効/無効
46+
- `true`の場合、選択した候補が次回から優先的に表示されます
47+
- 学習データは`lruFile`で指定したファイルに保存されます
48+
- **`lruFile`**: LRU学習データのファイル名
49+
- Application Support/MacTcode ディレクトリに自動保存されます
4350

4451
### 2. 部首変換設定 (`bushu`)
4552

4653
```json
4754
"bushu": {
4855
"bushuYomiCharacters": "0-9()、。「」・\\p{Hiragana}\\p{Katakana}\\p{Han}",
49-
"dictionaryFile": "bushu.dic"
56+
"dictionaryFile": "bushu.dic",
57+
"autoEnabled": false,
58+
"autoFile": "bushu_auto.dic"
5059
}
5160
```
5261

5362
- **`bushuYomiCharacters`**: 部首変換で部品取得に含める文字の正規表現文字クラス記法
5463
- **`dictionaryFile`**: 部首変換辞書のファイル名
64+
- **`autoEnabled`**: 部首変換自動学習機能の有効/無効
65+
- **`autoFile`**: 部首変換自動学習データのファイル名
5566

5667
### 3. キーバインド設定 (`keyBindings`)
5768

@@ -105,6 +116,7 @@ MacTcodeは`config.json`形式の設定ファイルを使用してカスタマ
105116
"excludedApplications": ["com.apple.loginwindow", "com.apple.SecurityAgent"],
106117
"disableOneYomiApplications": ["com.google.Chrome"],
107118
"syncStatsInterval": 1200,
119+
"cancelPeriod": 1.5,
108120
"keyboardLayout": "dvorak",
109121
"keyboardLayoutMapping": [
110122
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
@@ -126,6 +138,10 @@ MacTcodeは`config.json`形式の設定ファイルを使用してカスタマ
126138
- 0に設定すると統計ファイルの出力が無効になります
127139
- 統計情報は入力メソッド切り替え時やアプリケーション終了時に自動的に保存されます
128140
- 詳細はREADME.mdの「統計情報の記録」セクションを参照してください
141+
- **`cancelPeriod`**: 変換確定後のキャンセル可能期間(秒、0.1-10.0)
142+
- 変換確定後、この期間内であればDeleteキーやControl-g、Escapeキーで変換をキャンセルして読みに戻すことができます
143+
- デフォルト値: 1.5秒
144+
- 部首変換自動学習機能、LRU学習機能が有効な場合、この期間経過時に学習データが更新されます
129145
- **`keyboardLayout`**: キーボードレイアウトの名前("dvorak", "qwerty"等)
130146
- **`keyboardLayoutMapping`**: 40個のキー配列マッピング(文字列配列)
131147
- **`logEnabled`**: デバッグログの出力有効/無効

MacTcode.xcodeproj/project.pbxproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
/* Begin PBXBuildFile section */
1010
9D0F3CDF2C04D00F005A0CAC /* Bushu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0F3CDE2C04D00E005A0CAC /* Bushu.swift */; };
11-
9D0F3CE12C04D570005A0CAC /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0F3CE02C04D570005A0CAC /* Config.swift */; };
1211
9D0F3CE52C04DEAC005A0CAC /* BushuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0F3CE42C04DEAC005A0CAC /* BushuTests.swift */; };
1312
9D0F3CE72C04E139005A0CAC /* bushu.dic in Resources */ = {isa = PBXBuildFile; fileRef = 9D0F3CE62C04E139005A0CAC /* bushu.dic */; };
1413
9D4818DA2C0368DB00906504 /* main.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 9D4818D82C0368DB00906504 /* main.tiff */; };
14+
9D7B903C2EB729FD00DD7920 /* PendingKakutei.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7B903B2EB729FD00DD7920 /* PendingKakutei.swift */; };
1515
9D868A012C05F98700128D48 /* main@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 9D868A002C05F98700128D48 /* main@2x.tiff */; };
1616
9D868FF32C706AB200293645 /* ContextClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D868FF22C706AB200293645 /* ContextClientTests.swift */; };
1717
9D8BFEC82C0FA9DA00B99BA2 /* ZenkakuModeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8BFEC72C0FA9DA00B99BA2 /* ZenkakuModeTest.swift */; };
@@ -84,12 +84,12 @@
8484

8585
/* Begin PBXFileReference section */
8686
9D0F3CDE2C04D00E005A0CAC /* Bushu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bushu.swift; sourceTree = "<group>"; };
87-
9D0F3CE02C04D570005A0CAC /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
8887
9D0F3CE42C04DEAC005A0CAC /* BushuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BushuTests.swift; sourceTree = "<group>"; };
8988
9D0F3CE62C04E139005A0CAC /* bushu.dic */ = {isa = PBXFileReference; explicitFileType = text; fileEncoding = 4; path = bushu.dic; sourceTree = "<group>"; };
9089
9D4818D82C0368DB00906504 /* main.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = main.tiff; sourceTree = "<group>"; };
9190
9D6BA6A82C462793007524AC /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; };
9291
9D6BA6AA2C46279A007524AC /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
92+
9D7B903B2EB729FD00DD7920 /* PendingKakutei.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingKakutei.swift; sourceTree = "<group>"; };
9393
9D868A002C05F98700128D48 /* main@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "main@2x.tiff"; sourceTree = "<group>"; };
9494
9D868FF22C706AB200293645 /* ContextClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextClientTests.swift; sourceTree = "<group>"; };
9595
9D8BFEC72C0FA9DA00B99BA2 /* ZenkakuModeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZenkakuModeTest.swift; sourceTree = "<group>"; };
@@ -214,7 +214,6 @@
214214
9D8C8DE22C01ADA9003D8A43 /* Info.plist */,
215215
9DEA6D392C0AC7F90005BB18 /* Action.swift */,
216216
9DEAFE1E2C5F9E5F00E8B5E3 /* Command.swift */,
217-
9D0F3CE02C04D570005A0CAC /* Config.swift */,
218217
9D8BFECB2C15D64F00B99BA2 /* Log.swift */,
219218
9DEAFE312C5FB30400E8B5E3 /* Zenkaku */,
220219
9D4818D82C0368DB00906504 /* main.tiff */,
@@ -330,6 +329,7 @@
330329
9DEAFE302C5FB2EC00E8B5E3 /* Mode */ = {
331330
isa = PBXGroup;
332331
children = (
332+
9D7B903B2EB729FD00DD7920 /* PendingKakutei.swift */,
333333
9DEAFE262C5FA2A800E8B5E3 /* Controller.swift */,
334334
9DEA6D472C0EB0430005BB18 /* Mode.swift */,
335335
9DEAFE282C5FA2CF00E8B5E3 /* ModeWithCandidates.swift */,
@@ -500,9 +500,9 @@
500500
9DEAFE2D2C5FB24500E8B5E3 /* ZenkakuModeAction.swift in Sources */,
501501
9DEA6D462C0E45950005BB18 /* Client.swift in Sources */,
502502
9DA358E72E2DEF4100323184 /* PostfixBushuAction.swift in Sources */,
503+
9D7B903C2EB729FD00DD7920 /* PendingKakutei.swift in Sources */,
503504
9D0F3CDF2C04D00F005A0CAC /* Bushu.swift in Sources */,
504505
9DEAFE352C6DB3E000E8B5E3 /* ClientWrapper.swift in Sources */,
505-
9D0F3CE12C04D570005A0CAC /* Config.swift in Sources */,
506506
9D8BFECC2C15D64F00B99BA2 /* Log.swift in Sources */,
507507
9DEAFE272C5FA2A800E8B5E3 /* Controller.swift in Sources */,
508508
9DEA6D382C0AC0DA0005BB18 /* Keymap.swift in Sources */,
@@ -712,7 +712,7 @@
712712
"@executable_path/../Frameworks",
713713
);
714714
MACOSX_DEPLOYMENT_TARGET = 11.0;
715-
MARKETING_VERSION = 0.11.1;
715+
MARKETING_VERSION = 0.12.0;
716716
ONLY_ACTIVE_ARCH = NO;
717717
PRODUCT_BUNDLE_IDENTIFIER = "jp.mad-p.inputmethod.MacTcode";
718718
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -749,7 +749,7 @@
749749
"@executable_path/../Frameworks",
750750
);
751751
MACOSX_DEPLOYMENT_TARGET = 11.0;
752-
MARKETING_VERSION = 0.11.1;
752+
MARKETING_VERSION = 0.12.0;
753753
PRODUCT_BUNDLE_IDENTIFIER = "jp.mad-p.inputmethod.MacTcode";
754754
PRODUCT_NAME = "$(TARGET_NAME)";
755755
SWIFT_EMIT_LOC_STRINGS = YES;

MacTcode/AppDelegate.swift

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2424
// 統計情報の初期化
2525
_ = InputStats.shared
2626

27-
// シグナルハンドラの設定
28-
AppDelegate.setupSignalHandlers()
29-
3027
// アクセシビリティ権限の確認
3128
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
3229
let trusted = AXIsProcessTrustedWithOptions(options)
@@ -41,46 +38,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4138
func applicationWillTerminate(_ notification: Notification) {
4239
Log.i("★AppDelegate terminating self=\(ObjectIdentifier(self))")
4340
InputStats.shared.writeStatsToFile()
44-
}
45-
46-
/// シグナルハンドラを設定
47-
static func setupSignalHandlers() {
48-
// パイプ([読端, 書端])
49-
guard pipe(&termPipe) == 0 else { fatalError("pipe failed") }
50-
51-
NSLog("★Setting up signal handlers...")
52-
53-
// C呼出規約、キャプチャなし
54-
let sigtermHandler: @convention(c) (Int32) -> Void = { _ in
55-
var one: UInt8 = 1
56-
// write は async-signal-safe
57-
_ = withUnsafePointer(to: &one) {
58-
$0.withMemoryRebound(to: UInt8.self, capacity: 1) {
59-
write(termPipe[1], $0, 1)
60-
}
61-
}
62-
}
63-
64-
// シグナル登録
65-
signal(SIGINT, sigtermHandler)
66-
signal(SIGTERM, sigtermHandler)
67-
68-
// SIGINT用のDispatchSourceを作成
69-
let sigintSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
70-
sigintSource.setEventHandler {
71-
NSLog("★Received SIGINT, writing statistics...")
72-
InputStats.shared.writeStatsToFile()
73-
exit(0)
74-
}
75-
sigintSource.resume()
76-
77-
// SIGTERM用のDispatchSourceを作成
78-
let sigtermSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main)
79-
sigtermSource.setEventHandler {
80-
Log.i("★Received SIGTERM, writing statistics...")
81-
InputStats.shared.writeStatsToFile()
82-
exit(0)
83-
}
84-
sigtermSource.resume()
41+
MazegakiDict.i.saveLruData()
42+
Bushu.i.saveAutoData()
8543
}
8644
}

0 commit comments

Comments
 (0)