@@ -12,9 +12,27 @@ class InputStats {
1212 private var functionCount = 0
1313 private var totalActionCount = 0
1414
15+ // ストローク統計 (T-Code基本キーのみ)
16+ // per-key count (0..39)
17+ private var keyCount : [ Int ] = Array ( repeating: 0 , count: nKeys)
18+ // basic character frequency (1st*40 + 2nd -> 0..1599)
19+ private var basicCharCount : [ Int ] = Array ( repeating: 0 , count: nKeys * nKeys)
20+ // bigram (same shape as basicCharCount)
21+ private var bigramCount : [ Int ] = Array ( repeating: 0 , count: nKeys * nKeys)
22+ // panes frequency: "RL","RR","LL","LR"
23+ private var panes : [ String : Int ] = [ " RL " : 0 , " RR " : 0 , " LL " : 0 , " LR " : 0 ]
24+ // alternation counts
25+ private var alternation : [ String : Int ] = [ " alternate " : 0 , " consecutive " : 0 , " first " : 0 ]
26+
27+ // state for continuity
28+ private var lastStrokeKey : Int ? = nil
29+ private var lastStrokePane : String ? = nil // "L" or "R"
30+
1531 private let queue = DispatchQueue ( label: " jp.mad-p.inputmethods.MacTcode.inputstats " , attributes: . concurrent)
1632
1733 private init ( ) {
34+ // 初期化時に保存済みの stroke-stats.json を読み込む
35+ loadStrokeStatsMaybe ( )
1836 }
1937
2038 /// 基本文字入力のカウントを増やす
@@ -30,13 +48,17 @@ class InputStats {
3048 func incrementBushuCount( ) {
3149 queue. async ( flags: . barrier) {
3250 self . bushuCount += 1
51+ // continuity break
52+ self . recordNonStrokeEventInternal ( )
3353 }
3454 }
3555
3656 /// 交ぜ書き変換のカウントを増やす
3757 func incrementMazegakiCount( ) {
3858 queue. async ( flags: . barrier) {
3959 self . mazegakiCount += 1
60+ // continuity break
61+ self . recordNonStrokeEventInternal ( )
4062 }
4163 }
4264
@@ -45,6 +67,135 @@ class InputStats {
4567 queue. async ( flags: . barrier) {
4668 self . functionCount += 1
4769 self . totalActionCount += 1
70+ // continuity break
71+ self . recordNonStrokeEventInternal ( )
72+ }
73+ }
74+
75+ // MARK: - Stroke statistics API
76+
77+ /// ストローク(T-Code基本文字)を記録する
78+ /// - Parameter key1, key2: 0..(nKeys-1)
79+ func recordBasicStroke( key1: Int , key2: Int ) {
80+ guard UserConfigs . i. system. strokeStatsEnabled else { return }
81+ guard ( 0 ..< nKeys) . contains ( key1) else { return }
82+ guard ( 0 ..< nKeys) . contains ( key2) else { return }
83+ queue. async ( flags: . barrier) {
84+ let idx = key1 * nKeys + key2
85+ self . basicCharCount [ idx] += 1
86+ let pane1 = key1 % 10 < 5 ? " L " : " R "
87+ let pane2 = key2 % 10 < 5 ? " L " : " R "
88+ let pair = pane1 + pane2
89+ if self . panes [ pair] != nil {
90+ self . panes [ pair] ! += 1
91+ }
92+ }
93+ }
94+
95+ /// ストローク(T-Code基本キー)を記録する
96+ /// - Parameter key: 0..(nKeys-1)
97+ func recordStroke( key: Int ) {
98+ guard UserConfigs . i. system. strokeStatsEnabled else { return }
99+ guard ( 0 ..< nKeys) . contains ( key) else { return }
100+ queue. async ( flags: . barrier) {
101+ self . keyCount [ key] += 1
102+ // panes
103+ let pane = ( key % 10 ) < 5 ? " L " : " R "
104+ if let last = self . lastStrokeKey {
105+ // bigram
106+ let idx = last * nKeys + key
107+ self . bigramCount [ idx] += 1
108+ // panes pair
109+ if let lastPane = self . lastStrokePane {
110+ // alternation
111+ if lastPane == pane {
112+ self . alternation [ " consecutive " ] ! += 1
113+ } else {
114+ self . alternation [ " alternate " ] ! += 1
115+ }
116+ }
117+ } else {
118+ // first hit
119+ self . alternation [ " first " ] ! += 1
120+ }
121+ // update last
122+ self . lastStrokeKey = key
123+ self . lastStrokePane = pane
124+ }
125+ }
126+
127+ /// 非ストロークイベントを記録して連続性を断つ
128+ func recordNonStrokeEvent( ) {
129+ guard UserConfigs . i. system. strokeStatsEnabled else { return }
130+ queue. async ( flags: . barrier) {
131+ self . recordNonStrokeEventInternal ( )
132+ }
133+ }
134+
135+ // internal: assumes barrier queue
136+ private func recordNonStrokeEventInternal( ) {
137+ self . lastStrokeKey = nil
138+ self . lastStrokePane = nil
139+ }
140+
141+ /// ストローク統計をリセット(メモリ内)
142+ func resetStrokeStats( ) {
143+ queue. async ( flags: . barrier) {
144+ self . keyCount = Array ( repeating: 0 , count: nKeys)
145+ self . basicCharCount = Array ( repeating: 0 , count: nKeys * nKeys)
146+ self . bigramCount = Array ( repeating: 0 , count: nKeys * nKeys)
147+ self . panes = [ " RL " : 0 , " RR " : 0 , " LL " : 0 , " LR " : 0 ]
148+ self . alternation = [ " alternate " : 0 , " consecutive " : 0 , " first " : 0 ]
149+ self . lastStrokeKey = nil
150+ self . lastStrokePane = nil
151+ }
152+ }
153+
154+ // MARK: - Persistence (JSON)
155+
156+ private func strokeStatsFileURL( ) -> URL {
157+ return UserConfigs . i. configFileURL ( " stroke-stats.json " )
158+ }
159+
160+ func loadStrokeStatsMaybe( ) {
161+ guard UserConfigs . i. system. strokeStatsEnabled else { return }
162+ let fileURL = strokeStatsFileURL ( )
163+ guard FileManager . default. fileExists ( atPath: fileURL. path) else { return }
164+ queue. async ( flags: . barrier) {
165+ do {
166+ let data = try Data ( contentsOf: fileURL)
167+ if let obj = try JSONSerialization . jsonObject ( with: data, options: [ ] ) as? [ String : Any ] {
168+ if let k = obj [ " keyCount " ] as? [ Int ] , k. count == nKeys { self . keyCount = k }
169+ if let b = obj [ " basicCharCount " ] as? [ Int ] , b. count == nKeys * nKeys { self . basicCharCount = b }
170+ if let bg = obj [ " bigram " ] as? [ Int ] , bg. count == nKeys * nKeys { self . bigramCount = bg }
171+ if let p = obj [ " panes " ] as? [ String : Int ] { self . panes = p }
172+ if let a = obj [ " alternation " ] as? [ String : Int ] { self . alternation = a }
173+ }
174+ } catch {
175+ Log . i ( " Failed to load stroke-stats: \( error) " )
176+ }
177+ }
178+ }
179+
180+ func writeStrokeStatsToFile( ) {
181+ guard UserConfigs . i. system. strokeStatsEnabled else { return }
182+ queue. sync {
183+ let fileURL = strokeStatsFileURL ( )
184+ var obj : [ String : Any ] = [ : ]
185+ obj [ " keyCount " ] = self . keyCount
186+ obj [ " basicCharCount " ] = self . basicCharCount
187+ obj [ " bigram " ] = self . bigramCount
188+ obj [ " panes " ] = self . panes
189+ obj [ " alternation " ] = self . alternation
190+ obj [ " lastUpdated " ] = ISO8601DateFormatter ( ) . string ( from: Date ( ) )
191+
192+ do {
193+ let data = try JSONSerialization . data ( withJSONObject: obj, options: [ . prettyPrinted, . sortedKeys] )
194+ try data. write ( to: fileURL)
195+ } catch {
196+ Log . i ( " Failed to write stroke-stats: \( error) " )
197+ }
198+ Log . i ( " ★Stroke statistics written " )
48199 }
49200 }
50201
@@ -109,6 +260,9 @@ class InputStats {
109260 }
110261 }
111262
263+ // 同期タイミングでstroke-statsも書き出す(累積)
264+ self . writeStrokeStatsToFile ( )
265+
112266 Log . i ( " ★Statistics written: \( statsLine. trimmingCharacters ( in: . whitespacesAndNewlines) ) " )
113267 lastSyncDate = Date ( )
114268 basicCount = 0
0 commit comments