{"id":16833188,"url":"https://github.com/tmandry/swindler","last_synced_at":"2025-04-04T06:07:22.881Z","repository":{"id":43296615,"uuid":"44501746","full_name":"tmandry/Swindler","owner":"tmandry","description":"macOS window management library for Swift","archived":false,"fork":false,"pushed_at":"2023-12-31T22:55:44.000Z","size":3132,"stargazers_count":696,"open_issues_count":30,"forks_count":66,"subscribers_count":17,"default_branch":"main","last_synced_at":"2024-10-14T11:52:33.297Z","etag":null,"topics":["macos","osx","swift"],"latest_commit_sha":null,"homepage":"https://tmandry.github.io/Swindler/docs/main/","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tmandry.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2015-10-18T23:58:23.000Z","updated_at":"2024-10-08T19:09:47.000Z","dependencies_parsed_at":"2023-12-31T23:32:21.466Z","dependency_job_id":null,"html_url":"https://github.com/tmandry/Swindler","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmandry%2FSwindler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmandry%2FSwindler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmandry%2FSwindler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmandry%2FSwindler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tmandry","download_url":"https://codeload.github.com/tmandry/Swindler/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247128744,"owners_count":20888235,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["macos","osx","swift"],"created_at":"2024-10-13T11:52:39.656Z","updated_at":"2025-04-04T06:07:22.855Z","avatar_url":"https://github.com/tmandry.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Swindler\n_A Swift window management library for macOS_\n\n[![Build Status](https://travis-ci.com/tmandry/Swindler.svg?branch=main)](https://travis-ci.com/tmandry/Swindler)\n[![Join the chat at https://gitter.im/tmandry/Swindler](https://badges.gitter.im/tmandry/Swindler.svg)](https://gitter.im/tmandry/Swindler?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n\nIn the past few years, many developers formerly on Linux and Windows have migrated to Mac for their\nexcellent hardware and UNIX-based OS that \"just works\".\n\nBut along the way we gave up something dear to us: control over our desktop environment.\n\n**The goal of Swindler is to help us take back that control**, and give us the best of both worlds.\n\n## What Swindler Does\n\nWriting window managers for macOS is hard. There are a lot of systemic challenges, including limited\nand poorly-documented APIs. All window managers on macOS must use the C-based accessibility APIs, which\nare difficult to use and are surprisingly buggy themselves.\n\nAs a result, the selection of window managers is pretty limited, and many of the ones out there have\nannoying bugs, like freezes, race conditions, \"phantom windows\", and not \"seeing\" windows that are\nactually there. The more sophisticated the window manager is, the more it relies on these APIs and\nthe more these bugs start to show up.\n\nSwindler's job is to make it easy to write powerful window managers using a well-documented Swift\nAPI and abstraction layer. It addresses the problems of the accessibility API with these features:\n\n### Type safety\n\n[Swindler's API](https://github.com/tmandry/Swindler/blob/main/API.swift) is\nfully documented and type-safe thanks to Swift. It's much easier and safer to use than the C-based\naccessibility APIs. (See the example below.)\n\n### In-memory model\n\nWindow managers on macOS rely on IPC: you _ask_ an application for a window's position, _wait_ for it\nto respond, _request_ that it be moved or focused, then _wait_ for the application to comply (or\nnot). Most of the time this works okay, but it works at the mercy of the remote application's event\nloop, which can lead to long, multi-second delays.\n\nSwindler maintains a model of all applications and window states, so your code knows everything\nabout the windows on the screen. **Reads are instantaneous**, because all state is cached within your\napplication's process and stays up to date. Swindler is extensively tested to ensure it stays\nconsistent with the system in any situation.\n\n### Asynchronous writes and refreshes\n\nIf you need to resize a lot of windows simultaneously, for example, you can do so without fear of\none unresponsive application holding everything else up. Write requests are dispatched\nasynchronously and concurrently, and Swindler's promise-based API makes it easy to keep up with the\nstate of operations.\n\n### Friendly events\n\nMore sophisticated window managers have to observe events on windows, but the observer API is\nnot well documented and often leaves out events you might expect, or delivers them in the wrong order.\nFor example, the following situation is common when a new window pops up:\n\n```\n1. MainWindowChanged on com.google.chrome to \u003cwindow1\u003e\n2. WindowCreated on com.google.chrome: \u003cwindow1\u003e\n```\n\nSee the problem? With Swindler, all events are emitted in the expected order, and missing ones are\nfilled in. Swindler's in-memory state will always be consistent with itself and with the events you\nreceive, avoiding many bugs that are difficult to diagnose.\n\nAs a bonus, **events caused by your code are marked** as such, so you don't respond to them as user\nactions. This feature alone makes a whole new level of sophistication possible.\n\n## Example\n\nThe following code assigns all windows on the screen to a grid. Note the simplicity and power of the\npromise-based API. Requests are dispatched concurrently and in the background, not serially.\n\n```swift\nSwindler.initialize().then { state -\u003e Void in\n    let screen = state.screens.first!\n\n    let allPlacedOnGrid = screen.knownWindows.enumerate().map { index, window in\n        let rect = gridRect(screen, index)\n        return window.frame.set(rect)\n    }\n\n    when(allPlacedOnGrid) { _ in\n        print(\"all done!\")\n    }\n}.catch { error in\n    // ...\n}\n\nfunc gridRect(screen: Swindler.Screen, index: Int) -\u003e CGRect {\n    let gridSteps = 3\n    let position  = CGSize(width: screen.width / gridSteps,\n                           height: screen.height / gridSteps)\n    let size      = CGPoint(x: gridSize.width * (index % gridSteps),\n                            y: gridSize.height * (index / gridSteps))\n    return CGRect(origin: position, size: size)\n}\n```\n\nWatching for events is simple. Here's how you would implement snap-to-grid:\n\n```swift\nswindlerState.on { (event: WindowMovedEvent) in\n    guard event.external == true else {\n        // Ignore events that were caused by us.\n        return\n    }\n    let snapped = closestGridPosition(event.window.frame.value)\n    event.window.frame.value = snapped\n}\n```\n\n### Requesting permission\n\nYour application must request access to the trusted AX API. To do this, simply use\nthis code in your AppDelegate:\n\n```swift\nfunc applicationDidFinishLaunching(_ aNotification: Notification) {\n    guard AXSwift.checkIsProcessTrusted(prompt: true) else {\n        print(\"Not trusted as an AX process; please authorize and re-launch\")\n        NSApp.terminate(self)\n        return\n    }\n\n    // your code here\n}\n```\n\n### A note on error messages\n\nMany helper or otherwise \"special\"  app components don't respond to the AX requests\nor respond with an error. As a result, it's expected to see a number of messages\nlike this:\n\n```\n\u003cDebug\u003e: Window \u003cAXUnknown \"\u003cAXUIElement 0x610000054eb0\u003e {pid=464}\" (pid=464)\u003e has subrole AXUnknown, unwatching\n\u003cDebug\u003e: Application invalidated: com.apple.dock\n\u003cDebug\u003e: Couldn't initialize window for element \u003cAXUnknown \"\u003cAXUIElement 0x610000054eb0\u003e {pid=464}\" (pid=464)\u003e () of com.google.Chrome: windowIgnored(\u003cAXUnknown \"\u003cAXUIElement 0x610000054eb0\u003e {pid=464}\" (pid=464)\u003e)\n\u003cNotice\u003e: Could not watch application com.apple.dock (pid=308): invalidObject(AXError.NotificationUnsupported)\n\u003cDebug\u003e: Couldn't initialize window for element \u003cAXScrollArea \"\u003cAXUIElement 0x61800004ed90\u003e {pid=312}\" (pid=312)\u003e (desktop) of com.apple.finder: AXError.NotificationUnsupported\n```\n\nCurrently these are logged because it's hard to determine if an app \"should\" fail\n(especially on timeouts). As long as things appear to be working, you can ignore them.\n\n## Project Status\n\nSwindler is in development and is in **alpha**. Here is the state of its major features:\n\n- Asynchronous property system: **100% complete**\n- Event system: **100% complete**\n- Window API: **90% complete**\n- Application API: **90% complete**\n- Screen API: **90% complete**\n- Spaces API: **0% complete**\n\nYou can see the entire [planned API here](https://github.com/tmandry/Swindler/blob/main/API.swift).\n\n[API Documentation (latest release)](https://tmandry.github.io/Swindler/docs/latest)\n\n[API Documentation (main)](https://tmandry.github.io/Swindler/docs/main)\n\n## Development\n\nSwindler uses [Swift Package Manager](https://swift.org/package-manager/).\n\n### Building the project\n\nClone the project, then in your shell run:\n\n```\n$ cd Swindler\n$ git submodule init\n$ git submodule update\n```\n\nAt this point you should be able to build Swindler in Xcode and start on your way!\n\n### Using the command line\nYou can run the example project from the command line.\n\n```\nswift run\n```\n\n\n\n## Contact\n\nYou can chat with us on [Gitter](https://gitter.im/tmandry/Swindler).\n\nFollow me on Twitter: [@tmandry](https://twitter.com/tmandry)\n\n## Related Projects\n\n- [Silica](https://github.com/ianyh/Silica)\n- [Mjolnir](https://github.com/sdegutis/mjolnir)\n- [Hammerspoon](https://github.com/Hammerspoon/hammerspoon), a fork of Mjolnir\n\nSwindler is built on [AXSwift](https://github.com/tmandry/AXSwift).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftmandry%2Fswindler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftmandry%2Fswindler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftmandry%2Fswindler/lists"}