feat: handle cli commands: --list and --focus=window_id

This commit is contained in:
lwouis committed 2025-01-05 19:21:53 +01:00
1 parent d4e697c0de
commit 0f2c0e794c
6 files changed
+128

No files matched your search

+4
View File
@@ -130,6 +130,7 @@
BF0C894199BB262BDB2D8D37 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8221101CD179DB1121D5 /* InfoPlist.strings */; };
BF0C894325A54CF0B6B53464 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8B8B31D2F421AC258351 /* Localizable.strings */; };
BF0C896B5D08B7DA8F2024AC /* KeyRepeatTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8D606B53D315F2D9927B /* KeyRepeatTimer.swift */; };
BF0C8973EC078169170F2634 /* CliEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */; };
BF0C898511686611E4D7D81E /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8B2C057099EDABE46779 /* Button.swift */; };
BF0C89B4B17D7843C32A102E /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8353097840D7DB1E0E2E /* Markdown.swift */; };
BF0C89C49CFBAAB4646BB8CB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C82A8848224E76B880A83 /* InfoPlist.strings */; };
@@ -451,6 +452,7 @@
BF0C8C5210ACA9FBFAE9C631 /* preferences-general.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "preferences-general.jpg"; sourceTree = "<group>"; };
BF0C8C5B74E5DA2CC31E52F4 /* extract_l10n_strings.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = extract_l10n_strings.sh; sourceTree = "<group>"; };
BF0C8C5C0FF5D567BF190DE5 /* ApplicationServices.HIServices.framework.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationServices.HIServices.framework.swift; sourceTree = "<group>"; };
BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CliEvents.swift; sourceTree = "<group>"; };
BF0C8C6995992F837A9A83A6 /* ca */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ca; path = InfoPlist.strings; sourceTree = "<group>"; };
BF0C8C77273E00B019725640 /* add_missing_terms_on_poeditor.ts */ = {isa = PBXFileReference; lastKnownFileType = file.ts; path = add_missing_terms_on_poeditor.ts; sourceTree = "<group>"; };
BF0C8CA36F8BD99BF38A45D0 /* bn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = bn; path = InfoPlist.strings; sourceTree = "<group>"; };
@@ -1777,6 +1779,7 @@
BF0C869E544F968051715452 /* SystemAppearanceEvents.swift */,
BF0C8B5D1A9658E64F8ECB56 /* SystemScrollerStyleEvents.swift */,
BF0C8643F524515224BF1618 /* KeyboardEventsTestable.swift */,
BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */,
);
path = events;
sourceTree = "<group>";
@@ -2325,6 +2328,7 @@
BF0C8E366BC25171C0A3881C /* ApplicationServices.HIServices.framework.swift in Sources */,
BF0C803A13C409E7F01FBCAB /* SkyLight.framework.swift in Sources */,
BF0C80C69EFEF93C18539925 /* PreferencesMigrations.swift in Sources */,
BF0C8973EC078169170F2634 /* CliEvents.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+11
View File
@@ -314,3 +314,14 @@ extension NSPoint {
return NSPoint(x: lhs.x / Double(rhs), y: lhs.y / Double(rhs))
}
}
extension Optional {
enum Error: Swift.Error {
case unexpectedNil
}
// useful call multiple statements that could fail, and have a unique do-catch block to handle failures
func unwrapOrThrow() throws -> Wrapped {
if let self { return self } else { throw Error.unexpectedNil }
}
}
+3
View File
@@ -11,6 +11,7 @@ class BackgroundWork {
static var systemPermissionsThread: BackgroundThreadWithRunLoop!
static var repeatingKeyThread: BackgroundThreadWithRunLoop!
static var missionControlThread: BackgroundThreadWithRunLoop!
static var cliEventsThread: BackgroundThreadWithRunLoop!
static let screenshotsDispatchGroup = DispatchGroup()
// swift static variables are lazy; we artificially force the threads to init
@@ -29,6 +30,8 @@ class BackgroundWork {
repeatingKeyThread = BackgroundThreadWithRunLoop("repeatingKeyThread", .userInteractive)
// we main Mission Control state on a background thread. We protect reads from main-thread with an NSLock
missionControlThread = BackgroundThreadWithRunLoop("missionControlThread", .userInteractive)
// we listen to CLI commands (CFMessagePort events)
cliEventsThread = BackgroundThreadWithRunLoop("cliEventsThread", .userInteractive)
}
static func startCrashReportsQueue() {
+104
View File
@@ -0,0 +1,104 @@
class CliEvents {
static let portName = "com.lwouis.alt-tab-macos.cli"
static func observe() {
var context = CFMessagePortContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
if let messagePort = CFMessagePortCreateLocal(nil, portName as CFString, handleEvent, &context, nil),
let source = CFMessagePortCreateRunLoopSource(nil, messagePort, 0) {
CFRunLoopAddSource(BackgroundWork.cliEventsThread.runLoop, source, .commonModes)
} else {
Logger.error("Can't listen on message port. Is another AltTab already running?")
// TODO: should we quit or restart here?
// It's complex since AltTab can be restarted sometimes,
// and the new instance may coexit with the old for some duration
// There is also the case of multiple instances at login
}
}
}
fileprivate func handleEvent(_: CFMessagePort?, _: Int32, _ data: CFData?, _: UnsafeMutableRawPointer?) -> Unmanaged<CFData>? {
Logger.debug()
if let data = data,
let message = String(data: data as Data, encoding: .utf8) {
Logger.info(message)
let output = CliServer.executeCommandAndSendReponse(message)
if let responseData = try? CliServer.jsonEncoder.encode(output) as CFData {
return Unmanaged.passRetained(responseData)
}
}
Logger.error("Failed to decode message")
return nil
}
class CliServer {
static let jsonEncoder = JSONEncoder()
static let error = "error"
static let noOutput = "noOutput"
static func executeCommandAndSendReponse(_ rawValue: String) -> Codable {
var output: Codable = ""
DispatchQueue.main.sync {
if rawValue == "--list" {
output = JsonOutput(windows: Windows.list
.filter { !$0.isWindowlessApp }
.map { JsonWindow(id: $0.cgWindowId, title: $0.title) }
)
return
}
if rawValue.hasPrefix("--focus=") {
if let id = CGWindowID(rawValue.dropFirst("--focus=".count)),
let window = (Windows.list.first { $0.cgWindowId == id }) {
window.focus()
output = noOutput
return
}
}
output = error
}
return output
}
}
struct JsonOutput: Codable {
var windows: [JsonWindow]
}
struct JsonWindow: Codable {
var id: CGWindowID?
var title: String
}
class CliClient {
static func detectCommand() -> String? {
let args = CommandLine.arguments
if args.count == 2 && !args[1].starts(with: "--logs=") {
if args[1] == "--list" || args[1].hasPrefix("--focus=") {
return args[1]
}
}
return nil
}
static func sendCommandAndProcessResponse(_ command: String) {
do {
let serverPortClient = try CFMessagePortCreateRemote(nil, CliEvents.portName as CFString).unwrapOrThrow()
let data = try command.data(using: .utf8).unwrapOrThrow()
var returnData: Unmanaged<CFData>?
let _ = CFMessagePortSendRequest(serverPortClient, 0, data as CFData, 2, 2, CFRunLoopMode.defaultMode.rawValue, &returnData)
let responseData = try returnData.unwrapOrThrow().takeRetainedValue()
if let response = String(data: responseData as Data, encoding: .utf8) {
if response != "\"\(CliServer.error)\"" {
if response != "\"\(CliServer.noOutput)\"" {
print(response)
}
exit(0)
}
}
print("Couldn't execute command. Is it correct?")
exit(1)
} catch {
print("AltTab.app needs to be running for CLI commands to work")
exit(1)
}
}
}
+4
View File
@@ -1,6 +1,10 @@
import AppKit
import Darwin
if let command = CliClient.detectCommand() {
CliClient.sendCommandAndProcessResponse(command)
}
// - if the app is quit/force-quit from Activity Monitor, it will receive SIGTERM and applicationWillTerminate won't be called
// - if the app crashes in swift code (e.g. unexpected nil object), SIGTRAP is sent
// we intercept these signals, and do an emergency exit
+2
View File
@@ -317,7 +317,9 @@ extension App: NSApplicationDelegate {
KeyboardEvents.addEventHandlers()
MouseEvents.observe()
TrackpadEvents.observe()
CliEvents.observe()
self.preloadWindows()
Logger.info("AltTab ready")
#if DEBUG
// self.showPreferencesWindow()
#endif