Skip to content
Draft
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
0a52564
-
KishanBagaria May 31, 2026
3623f1c
-
KishanBagaria May 31, 2026
bed6418
-
KishanBagaria May 31, 2026
3c3368e
-
KishanBagaria May 31, 2026
299dbba
-
KishanBagaria May 31, 2026
57d8a62
-
KishanBagaria May 31, 2026
87e6832
-
KishanBagaria May 31, 2026
fb8607d
-
KishanBagaria May 31, 2026
46e6f05
-
KishanBagaria May 31, 2026
ea4da68
-
KishanBagaria May 31, 2026
c608f7d
-
KishanBagaria May 31, 2026
9f6ffee
-
KishanBagaria May 31, 2026
e3b348c
Update NSWorkspace+AsyncOpen.swift
KishanBagaria May 31, 2026
339e1c5
toggleThreadRead: run restorePinsIfNecessary if the initial pin actio…
indent[bot] May 31, 2026
a8f6134
-
KishanBagaria May 31, 2026
fa71175
Update MacPermissions.swift
KishanBagaria May 31, 2026
7d149e8
MessagesAppElements: sleep 0.5s after fire-and-forget deep link reque…
indent[bot] May 31, 2026
498e4fb
-
KishanBagaria May 31, 2026
af2a75f
Update MessagesController.swift
KishanBagaria May 31, 2026
39d5074
Update MessagesController.swift
KishanBagaria May 31, 2026
fc70b8c
-
KishanBagaria May 31, 2026
f0e7c4d
-
KishanBagaria May 31, 2026
e8a5411
-
KishanBagaria May 31, 2026
6d07601
-
KishanBagaria May 31, 2026
369050d
Merge branch 'main' into kb/modern-concurrency
KishanBagaria May 31, 2026
6e9618c
-
KishanBagaria May 31, 2026
e86f9d9
-
KishanBagaria May 31, 2026
f0c19b1
-
KishanBagaria May 31, 2026
7d52d80
-
KishanBagaria May 31, 2026
d31bd24
-
KishanBagaria May 31, 2026
8feab6d
Update MessagesAppElements.swift
KishanBagaria May 31, 2026
e997b10
Update MessagesController.swift
KishanBagaria May 31, 2026
e49acf9
Update todos.md
KishanBagaria May 31, 2026
4beb336
fix crash
KishanBagaria May 31, 2026
7a5d64f
-
KishanBagaria May 31, 2026
39d1e43
-
KishanBagaria May 31, 2026
3859417
-
KishanBagaria May 31, 2026
4ee2940
-
KishanBagaria Jun 1, 2026
5434835
-
KishanBagaria Jun 1, 2026
ac6a72d
Merge branch 'main' into kb/modern-concurrency
KishanBagaria Jun 2, 2026
2a4da1a
Update MessagesController.swift
KishanBagaria Jun 2, 2026
aee5d5a
use async method
pmanot Jun 3, 2026
b82a884
refactor
pmanot Jun 3, 2026
0411378
Revert "refactor"
KishanBagaria Jun 3, 2026
dfc5b75
Merge branch 'main' into kb/modern-concurrency
KishanBagaria Jun 3, 2026
49e129b
wip
pmanot Jun 3, 2026
5d8f9de
Merge branch 'kb/modern-concurrency' of https://github.com/beeper/pla…
pmanot Jun 3, 2026
74e7971
Revert "wip"
KishanBagaria Jun 3, 2026
fd743b8
improve `waitForLaunch`
pmanot Jun 3, 2026
6c6494b
-
KishanBagaria Jun 3, 2026
8e5577f
rcs
KishanBagaria Jun 4, 2026
3e745a0
-
KishanBagaria Jun 4, 2026
8758f48
always keep track of lastOpenedThreadID, reset when user opens messag…
KishanBagaria Jun 4, 2026
7257425
Update PlatformAPI.swift
KishanBagaria Jun 4, 2026
5aeefee
Update MessagesController.swift
KishanBagaria Jun 4, 2026
3b452a2
Update src/IMessage/Sources/IMessage/PromptAutomation.swift
pmanot Jun 4, 2026
d8572c1
fix broken commit
pmanot Jun 4, 2026
2a4275d
add withTimeout primitive
pmanot Jun 5, 2026
b8b7b0a
expose KVO through AsyncStream
pmanot Jun 5, 2026
74e97c5
rewrite waitForLaunch
pmanot Jun 5, 2026
aef2925
improve LaunchServices code + add timeouts
pmanot Jun 5, 2026
eae55f9
Merge branch 'main' of https://github.com/beeper/platform-imessage in…
pmanot Jun 6, 2026
b31da6c
fix timeout (wip)
pmanot Jun 6, 2026
c4afe97
always open chat, even if we don't want to observe
KishanBagaria Jun 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 1 addition & 52 deletions src/IMessage/Sources/IMDatabaseTestBench/TestBench.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct TestBench: AsyncParsableCommand {

static let configuration = CommandConfiguration(
abstract: "Exercise functionality in IMDatabase.",
subcommands: [Watch.self, Messages.self, Chats.self, UnreadBenchmark.self, FSEventsCommand.self, TestIdleAware.self, ClosestSelectable.self],
subcommands: [Watch.self, Messages.self, Chats.self, UnreadBenchmark.self, FSEventsCommand.self, ClosestSelectable.self],
)

mutating func run() async throws {}
Expand Down Expand Up @@ -430,54 +430,3 @@ extension TestBench {
}
}
}

// MARK: - Idle Aware

extension TestBench {
struct TestIdleAware: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "test-idle-aware",
abstract: "Tests the idle aware queue."
)
@OptionGroup var options: TestBench.Options

mutating func run() async throws {
bootstrap(logLevel: options.logLevel)

let queue = PassivelyAwareDispatchQueue(label: "test", idleDelay: 0.1)

queue.setIdleCallback { info in
print("*** IDLE! *** [0.1s] <\(info)>")
Thread.sleep(forTimeInterval: 0.1)
}

queue.async {
print("1. [1s]")
Thread.sleep(forTimeInterval: 1)
}
queue.async {
print("2. [0.5s]")
Thread.sleep(forTimeInterval: 0.5)
}
queue.async {
print("3. [0.25s]")
Thread.sleep(forTimeInterval: 0.25)
}

Task {
while true {
let ms = Int.random(in: 500...4000)
try! await Task.sleep(nanoseconds: UInt64(1_000_000 * ms))

queue.async {
let cost = Double.random(in: 0.5...1)
print("r. [\(cost)s]")
Thread.sleep(forTimeInterval: cost)
}
}
}

await Task.never()
}
}
}
2 changes: 1 addition & 1 deletion src/IMessage/Sources/IMessage/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ enum DefaultsKeys {
/** forces a specific coordinator (`eclipsing` or `spaces`), only checked once */
static let coordinator = "BEEPWindowCoordinator"

/** whether to respect calls to `onThreadSelected`/`watchThreadActivity` */
/** whether to respect calls to `onThreadSelected` for thread activity observation */
static let watchThreadActivity = "BEEPWatchThreadActivity"
/** whether to continuously poll "activity status" (typing indicator, dnd banner) */
static let pollActivityStatus = "BEEPPollActivityStatus"
Expand Down
31 changes: 19 additions & 12 deletions src/IMessage/Sources/IMessage/Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import Combine
import IMessageCore

extension NSApplication {
Expand All @@ -14,20 +15,25 @@ extension NSApplication {
}

extension NSRunningApplication {
func waitForLaunch(interval: TimeInterval = 0.05, timeout seconds: TimeInterval = 5) throws {
let start = Date()
while !self.isFinishedLaunching {
Log.default.notice("sleeping \(interval)s for \(String(describing: self.localizedName)) to finish launching")
Thread.sleep(forTimeInterval: interval)
if self.isTerminated {
throw ErrorMessage("\(String(describing: self.localizedName)) terminated")
}
if start.timeIntervalSinceNow < -seconds {
Log.default.notice("assuming \(String(describing: self.localizedName)) has launched") // sometimes this gets stuck in an infinite loop
break
func waitForLaunch(timeout seconds: TimeInterval = 5) async throws {
var cancellable: AnyCancellable?
Comment thread
indent[bot] marked this conversation as resolved.
Outdated
_ = cancellable

return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
if isFinishedLaunching {
continuation.resume()
} else {
cancellable = self.publisher(for: \.isFinishedLaunching, options: [.initial, .new])
.filter { $0 } // we only care about isFinishedLaunching = true
.first()
.timeout(.seconds(seconds), scheduler: DispatchQueue.global())
.sink { _ in
continuation.resume()
} receiveValue: { _ in

Comment thread
indent[bot] marked this conversation as resolved.
Outdated
}
}
}
Thread.sleep(forTimeInterval: 0.01)
}
}

Expand Down Expand Up @@ -60,6 +66,7 @@ extension NSRect {
/// primary display, the space used by Accessibility window positions and
/// CGWindow bounds). The flip is about the primary display's height, so it is
/// its own inverse and is correct regardless of which display the rect is on.
@MainActor
func flippedBetweenCocoaAndScreenSpace() -> NSRect {
guard let primaryHeight = NSScreen.screens.first?.frame.height else { return self }
return NSRect(x: minX, y: primaryHeight - maxY, width: width, height: height)
Expand Down
2 changes: 1 addition & 1 deletion src/IMessage/Sources/IMessage/IMessageHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public enum IMessageHost {

public static func disableMessagesNotifications() async throws {
try await Task.detached(priority: .background) {
_ = try PromptAutomation.disableNotificationsForApp(named: "Messages")
_ = try await PromptAutomation.disableNotificationsForApp(named: "Messages")
Defaults.playSoundEffects = false
}.value
}
Expand Down
63 changes: 34 additions & 29 deletions src/IMessage/Sources/IMessage/KeyPresser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,105 +6,110 @@ import Logging

private let log = Logger(imessageLabel: "key-presser")

// TODO: refactor
class KeyPresser {
let pid: pid_t
private let postKeyEvents: (CGKeyCode, CGEventFlags?) throws -> Void
private let keyCodeForCharacter: (Character) -> UInt16?

init(pid: pid_t) {
init(
pid: pid_t,
postKeyEvents: ((CGKeyCode, CGEventFlags?) throws -> Void)? = nil,
keyCodeForCharacter: ((Character) -> UInt16?)? = nil
) {
self.pid = pid
self.postKeyEvents = postKeyEvents ?? { key, flags in
try Self.post(key: key, flags: flags, to: pid)
}
self.keyCodeForCharacter = keyCodeForCharacter ?? { KeyMap.shared[$0] }
}

static let src = CGEventSource(stateID: .hidSystemState)

private func perform(onMainThread: Bool, _ action: () throws -> Void) rethrows {
private func perform<T>(onMainThread: Bool, _ action: () throws -> T) rethrows -> T {
guard onMainThread, !Thread.isMainThread else {
try action()
return
return try action()
}
log.debug("dispatching simulated keypress to main thread (queueName=\(__dispatch_queue_get_label(nil)))")
try DispatchQueue.main.sync {
return try DispatchQueue.main.sync {
try action()
}
return
}

private func post(key: CGKeyCode, flags: CGEventFlags? = nil) throws {
private static func post(key: CGKeyCode, flags: CGEventFlags? = nil, to pid: pid_t) throws {
log.debug("sending simulated keypress (code=\(key))")
for keyDown in [true, false] {
log.debug("simulated keypress phase (code=\(key), down=\(keyDown))")
// all events will not be posted for _some_ users if `keyboardEventSource` is nil
let ev = try CGEvent(keyboardEventSource: Self.src, virtualKey: key, keyDown: keyDown)
.orThrow(ErrorMessage("key \(key) event empty"))
if let flags { ev.flags = flags }
ev.postToPid(self.pid)
ev.postToPid(pid)
if isSequoiaOrUp, !keyDown { // workaround courtesy https://github.com/pmanot
ev.flags = []
ev.postToPid(self.pid)
ev.postToPid(pid)
}
}
}

private func press(key: CGKeyCode, flags: CGEventFlags? = nil, onMainThread: Bool = true) throws {
private func press(key: CGKeyCode, flags: CGEventFlags? = nil, onMainThread: Bool) throws {
try perform(onMainThread: onMainThread) {
try post(key: key, flags: flags)
try postKeyEvents(key, flags)
}
}

private func pressMappedKey(_ key: Character, flags: CGEventFlags? = nil, onMainThread: Bool = true) throws {
try perform(onMainThread: onMainThread) {
guard let keyCode = KeyMap.shared[key] else { return }
try post(key: CGKeyCode(keyCode), flags: flags)
}
private func pressMappedKey(_ key: Character, flags: CGEventFlags? = nil, onMainThread: Bool) throws {
guard let keyCode = perform(onMainThread: true, { keyCodeForCharacter(key) }) else { return }
try press(key: CGKeyCode(keyCode), flags: flags, onMainThread: onMainThread)
}

func `return`(onMainThread: Bool = true) throws {
func `return`(onMainThread: Bool = false) throws {
try press(key: CGKeyCode(kVK_Return), onMainThread: onMainThread)
}

func downArrow(onMainThread: Bool = true) throws {
func downArrow(onMainThread: Bool = false) throws {
try press(key: CGKeyCode(kVK_DownArrow), onMainThread: onMainThread)
}

func rightArrow(onMainThread: Bool = true) throws {
func rightArrow(onMainThread: Bool = false) throws {
try press(key: CGKeyCode(kVK_RightArrow), onMainThread: onMainThread)
}

func commandV(onMainThread: Bool = true) throws {
func commandV(onMainThread: Bool = false) throws {
// sending CGKeyCode(kVK_ANSI_V) won't work on non-qwerty layouts where V key is in a different place
try pressMappedKey("v", flags: .maskCommand, onMainThread: onMainThread)
}

/// marks as read/unread on ventura
func commandShiftU(onMainThread: Bool = true) throws {
func commandShiftU(onMainThread: Bool = false) throws {
try pressMappedKey("u", flags: [.maskCommand, .maskShift], onMainThread: onMainThread)
}

/// selects next thread, both keys aren't the same in practice
func commandRightBracket(onMainThread: Bool = true) throws {
func commandRightBracket(onMainThread: Bool = false) throws {
try pressMappedKey("]", flags: .maskCommand, onMainThread: onMainThread)
}

#if false
/// selects first thread
func command1(onMainThread: Bool = true) throws {
func command1(onMainThread: Bool = false) throws {
try pressMappedKey("1", flags: .maskCommand, onMainThread: onMainThread)
}
/// edits selected message
func commandE(onMainThread: Bool = true) throws {
func commandE(onMainThread: Bool = false) throws {
try pressMappedKey("e", flags: .maskCommand, onMainThread: onMainThread)
}
/// selects prev thread, both keys aren't the same in practice
func commandLeftBracket(onMainThread: Bool = true) throws {
func commandLeftBracket(onMainThread: Bool = false) throws {
try pressMappedKey("[", flags: .maskCommand, onMainThread: onMainThread)
}
/// selects first non-pinned thread
func commandOption1(onMainThread: Bool = true) throws {
func commandOption1(onMainThread: Bool = false) throws {
try pressMappedKey("1", flags: [.maskCommand, .maskAlternate], onMainThread: onMainThread)
}
func ctrlShiftTab(onMainThread: Bool = true) throws {
func ctrlShiftTab(onMainThread: Bool = false) throws {
try press(key: CGKeyCode(kVK_Tab), flags: [.maskControl, .maskShift], onMainThread: onMainThread)
}
func ctrlTab(onMainThread: Bool = true) throws {
func ctrlTab(onMainThread: Bool = false) throws {
try press(key: CGKeyCode(kVK_Tab), flags: .maskControl, onMainThread: onMainThread)
}
#endif
Expand Down
1 change: 1 addition & 0 deletions src/IMessage/Sources/IMessage/MacPermissions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public enum MacPermissions {
}

public static func askForAutomationAccess() async throws {
// Automation's TCC prompt is unreliable when OSA runs off the main actor.
try await MainActor.run {
try OSA.promptAutomationAccess()
}
Expand Down
Loading