feat: handle cli commands: --list and --focus=window_id
This commit is contained in:
1 parent
d4e697c0de
commit
0f2c0e794c
6 files changed
+128
No files matched your search
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user