Skip to content

Commit dccec88

Browse files
authored
Merge pull request #16 from sonya-sunyoung/feat/per-message-date-axhelp
feat(read): emit per-message date from time-label AXHelp tooltip
2 parents 9e9f115 + ff4d9a9 commit dccec88

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

Sources/kmsg/Accessibility/UIElement.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ public final class UIElement: @unchecked Sendable {
109109
attributeOptional(kAXDescriptionAttribute)
110110
}
111111

112+
/// AXHelp tooltip text. KakaoTalk attaches the full send date
113+
/// (e.g. "2026. 6. 2.") to each message's time label here.
114+
public var helpText: String? {
115+
attributeOptional(kAXHelpAttribute)
116+
}
117+
112118
public var identifier: String? {
113119
attributeOptional(kAXIdentifierAttribute)
114120
}

Sources/kmsg/KakaoTalk/TranscriptReader.swift

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@ struct TranscriptMessage: Encodable, Equatable, Sendable {
77
let body: String
88
let isSystem: Bool
99
let logicalTimestamp: Date?
10+
/// Calendar date of the message ("YYYY-MM-DD"), read from the time
11+
/// label's AXHelp tooltip. nil when the tooltip was unavailable.
12+
let date: String?
1013

1114
enum CodingKeys: String, CodingKey {
1215
case author
1316
case timeRaw = "time_raw"
1417
case body
18+
case date
1519
}
1620

1721
func encode(to encoder: Encoder) throws {
1822
var container = encoder.container(keyedBy: CodingKeys.self)
1923
try container.encode(author ?? "(me)", forKey: .author)
2024
try container.encodeIfPresent(timeRaw, forKey: .timeRaw)
2125
try container.encode(body, forKey: .body)
26+
try container.encodeIfPresent(date, forKey: .date)
2227
}
2328
}
2429

@@ -202,6 +207,9 @@ struct KakaoTalkTranscriptReader {
202207
var leftAnchorAuthor: String?
203208
var leftAnchorTimeRaw: String?
204209
var currentDateAnchor: Date?
210+
// AXHelp tooltip date ("YYYY-MM-DD") carried forward so consecutive
211+
// messages that share a single time label inherit the same day.
212+
var lastKnownDate: String?
205213

206214
for (offset, analysis) in analyses.enumerated() {
207215
let side = analysis.side
@@ -242,13 +250,19 @@ struct KakaoTalkTranscriptReader {
242250
continue
243251
}
244252

253+
if let axHelpDate = analysis.axHelpDate {
254+
lastKnownDate = axHelpDate
255+
}
256+
let resolvedDate = analysis.axHelpDate ?? lastKnownDate
257+
245258
if analysis.isSystemLikeRow {
246259
let message = TranscriptMessage(
247260
author: nil,
248261
timeRaw: analysis.timeRaw,
249262
body: bodyCandidate.body,
250263
isSystem: true,
251-
logicalTimestamp: currentDateAnchor
264+
logicalTimestamp: currentDateAnchor,
265+
date: resolvedDate
252266
)
253267
messages.append(message)
254268
continue
@@ -283,7 +297,8 @@ struct KakaoTalkTranscriptReader {
283297
for: resolvedTime,
284298
dateAnchor: currentDateAnchor,
285299
referenceDate: referenceDate
286-
)
300+
),
301+
date: resolvedDate
287302
)
288303
messages.append(message)
289304
if selectedLogs < 10 {
@@ -322,6 +337,7 @@ struct KakaoTalkTranscriptReader {
322337
var metadataTokensBuffer: [String] = []
323338
var buttonTitlesBuffer: [String] = []
324339
var imageFrames: [CGRect] = []
340+
var rowHelpDate: String?
325341

326342
for container in containers {
327343
var textAreas: [UIElement] = []
@@ -369,6 +385,9 @@ struct KakaoTalkTranscriptReader {
369385
}
370386

371387
for staticText in staticTexts {
388+
if rowHelpDate == nil, let help = staticText.helpText, let parsed = Self.parseHelpDate(help) {
389+
rowHelpDate = parsed
390+
}
372391
let normalized = normalizeBodyText(staticText.stringValue)
373392
guard !normalized.isEmpty else { continue }
374393
metadataTokensBuffer.append(contentsOf: metadataTokens(from: normalized))
@@ -436,7 +455,8 @@ struct KakaoTalkTranscriptReader {
436455
timeRaw: metadata.timeRaw,
437456
side: side,
438457
rowFrame: cachedRowFrame,
439-
isSystemLikeRow: systemLikeRow
458+
isSystemLikeRow: systemLikeRow,
459+
axHelpDate: rowHelpDate
440460
)
441461
}
442462

@@ -469,7 +489,8 @@ struct KakaoTalkTranscriptReader {
469489
for: metadata.timeRaw,
470490
dateAnchor: nil,
471491
referenceDate: referenceDate
472-
)
492+
),
493+
date: row.flatMap { axHelpDate(in: $0) }
473494
)
474495
)
475496
}
@@ -486,7 +507,8 @@ struct KakaoTalkTranscriptReader {
486507
timeRaw: nil,
487508
body: title,
488509
isSystem: false,
489-
logicalTimestamp: nil
510+
logicalTimestamp: nil,
511+
date: nil
490512
)
491513
)
492514
}
@@ -850,6 +872,35 @@ struct KakaoTalkTranscriptReader {
850872
return nil
851873
}
852874

875+
/// Parse the AXHelp tooltip KakaoTalk attaches to a message's time label,
876+
/// e.g. "2026. 6. 2." -> "2026-06-02".
877+
private static func parseHelpDate(_ text: String) -> String? {
878+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
879+
guard let match = trimmed.range(
880+
of: #"(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\.?"#,
881+
options: .regularExpression
882+
) else {
883+
return nil
884+
}
885+
let numbers = trimmed[match]
886+
.split(whereSeparator: { !$0.isNumber })
887+
.compactMap { Int($0) }
888+
guard numbers.count >= 3 else { return nil }
889+
return String(format: "%04d-%02d-%02d", numbers[0], numbers[1], numbers[2])
890+
}
891+
892+
/// Scan a row's static texts for the first AXHelp date tooltip.
893+
/// Used by the fallback message path, which lacks the per-row analysis.
894+
private func axHelpDate(in row: UIElement) -> String? {
895+
let staticTexts = row.findAll(role: kAXStaticTextRole, limit: 12, maxNodes: 240)
896+
for staticText in staticTexts {
897+
if let help = staticText.helpText, let parsed = Self.parseHelpDate(help) {
898+
return parsed
899+
}
900+
}
901+
return nil
902+
}
903+
853904
private func firstAncestor(of element: UIElement, role: String, maxHops: Int) -> UIElement? {
854905
var cursor: UIElement? = element
855906
var hops = 0
@@ -1017,6 +1068,7 @@ private struct RowAnalysis {
10171068
let side: MessageSide
10181069
let rowFrame: CGRect?
10191070
let isSystemLikeRow: Bool
1071+
let axHelpDate: String?
10201072

10211073
var referenceFrame: CGRect? {
10221074
bodyCandidate?.frame ?? rowFrame

0 commit comments

Comments
 (0)