Repository: ianyh/Amethyst Branch: development Commit: 9d87018cedc2 Files: 146 Total size: 900.9 KB Directory structure: gitextract_92so_qqv/ ├── .amethyst.sample.yml ├── .eslintrc.yml ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── tests.yml ├── .gitignore ├── .hound.yml ├── .swiftlint.yml ├── Amethyst/ │ ├── Amethyst-Bridging-Header.h │ ├── Amethyst-Info.plist │ ├── Amethyst.entitlements │ ├── AmethystDebug.entitlements │ ├── AppDelegate.swift │ ├── Base.lproj/ │ │ └── MainMenu.xib │ ├── Categories/ │ │ ├── NSRunningApplication+Manageable.swift │ │ └── NSTableView+Amethyst.swift │ ├── Debug/ │ │ ├── AppsInfo.swift │ │ ├── DebugInfo.swift │ │ ├── ScreensInfo.swift │ │ └── WindowsInfo.swift │ ├── Events/ │ │ └── HotKeyManager.swift │ ├── Images.xcassets/ │ │ ├── 123.rectangle.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── amethyst.imageset/ │ │ │ └── Contents.json │ │ ├── dots.and.line.vertical.and.cursorarrow.rectangle.imageset/ │ │ │ └── Contents.json │ │ ├── icon-statusitem-disabled.imageset/ │ │ │ └── Contents.json │ │ ├── icon-statusitem.imageset/ │ │ │ └── Contents.json │ │ ├── macwindow.on.rectangle.imageset/ │ │ │ └── Contents.json │ │ ├── text.and.command.macwindow.imageset/ │ │ │ └── Contents.json │ │ ├── uiwindow.split.2x1.imageset/ │ │ │ └── Contents.json │ │ └── waveform.path.ecg.rectangle.imageset/ │ │ └── Contents.json │ ├── Layout/ │ │ ├── Layout.swift │ │ ├── Layouts/ │ │ │ ├── BinarySpacePartitioningLayout.swift │ │ │ ├── ColumnLayout.swift │ │ │ ├── CustomLayout.swift │ │ │ ├── FloatingLayout.swift │ │ │ ├── FourColumnLayout.swift │ │ │ ├── FullscreenLayout.swift │ │ │ ├── RowLayout.swift │ │ │ ├── TallLayout.swift │ │ │ ├── TallRightLayout.swift │ │ │ ├── ThreeColumnLayout.swift │ │ │ ├── TwoPaneLayout.swift │ │ │ ├── TwoPaneRightLayout.swift │ │ │ ├── WideLayout.swift │ │ │ └── WidescreenTallLayout.swift │ │ └── ReflowOperation.swift │ ├── Managers/ │ │ ├── AppManager.swift │ │ ├── FocusFollowsMouseManager.swift │ │ ├── FocusTransitionCoordinator.swift │ │ ├── HotKeyRegistrar.swift │ │ ├── LayoutType.swift │ │ ├── LogManager.swift │ │ ├── ScreenManager.swift │ │ ├── Screens.swift │ │ ├── WindowManager.swift │ │ ├── WindowTransitionCoordinator.swift │ │ └── Windows.swift │ ├── Model/ │ │ ├── Application.swift │ │ ├── ApplicationEventHandler.swift │ │ ├── ApplicationObservation.swift │ │ ├── CGInfo.swift │ │ ├── Change.swift │ │ ├── MouseState.swift │ │ ├── Reliability.swift │ │ ├── Screen.swift │ │ ├── Space.swift │ │ ├── Window.swift │ │ └── WindowsInformation.swift │ ├── Preferences/ │ │ ├── DebugPreferencesViewController.swift │ │ ├── DebugPreferencesViewController.xib │ │ ├── FloatingPreferencesViewController.swift │ │ ├── FloatingPreferencesViewController.xib │ │ ├── GeneralPreferencesViewController.swift │ │ ├── GeneralPreferencesViewController.xib │ │ ├── LayoutsPreferencesViewController.swift │ │ ├── LayoutsPreferencesViewController.xib │ │ ├── MousePreferencesViewController.swift │ │ ├── MousePreferencesViewController.xib │ │ ├── ShortcutsPreferencesListItemView.swift │ │ ├── ShortcutsPreferencesViewController.swift │ │ ├── ShortcutsPreferencesViewController.xib │ │ └── UserConfiguration.swift │ ├── View/ │ │ ├── LayoutNameWindow.swift │ │ ├── LayoutNameWindow.xib │ │ ├── LayoutNameWindowController.swift │ │ └── PreferencesWindow.swift │ ├── default.amethyst │ ├── en.lproj/ │ │ ├── Credits.rtf │ │ └── InfoPlist.strings │ └── main.swift ├── Amethyst.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ ├── Amethyst Debug CLI.xcscheme │ └── Amethyst.xcscheme ├── Amethyst.xctestplan ├── Amethyst.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ ├── IDEWorkspaceChecks.plist │ └── swiftpm/ │ └── Package.resolved ├── AmethystTests/ │ ├── AmethystTests-Bridging-Header.h │ ├── Helpers/ │ │ ├── FrameAssignmentVerification.swift │ │ └── TestBundle.swift │ ├── Info.plist │ ├── Model/ │ │ ├── CustomLayouts/ │ │ │ ├── extended.js │ │ │ ├── fullscreen.js │ │ │ ├── null.js │ │ │ ├── recommended-main-pane-ratio.js │ │ │ ├── static-ratio-tall-native-commands.js │ │ │ ├── static-ratio-tall.js │ │ │ ├── subset.js │ │ │ ├── undefined.js │ │ │ └── uniform-columns.js │ │ ├── TestScreen.swift │ │ └── TestWindow.swift │ └── Tests/ │ ├── Categories/ │ │ └── SIWindow+AmethystTests.swift │ ├── Configuration/ │ │ └── UserConfigurationTests.swift │ ├── Layout/ │ │ ├── BinarySpacePartitioningLayoutTests.swift │ │ ├── ColumnLayoutTests.swift │ │ ├── CustomLayoutTests.swift │ │ ├── FloatingLayoutTests.swift │ │ ├── FullscreenLayoutTests.swift │ │ ├── RowLayoutTests.swift │ │ ├── TallLayoutTests.swift │ │ ├── TallRightLayoutTests.swift │ │ ├── ThreeColumnLayoutTests.swift │ │ ├── TwoPaneLayoutTests.swift │ │ ├── WideLayoutTests.swift │ │ └── WidescreenTallLayoutTests.swift │ └── Managers/ │ ├── HotKeyManagerTests.swift │ └── ScreenManagerTests.swift ├── Brewfile ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── docs/ │ ├── configuration-files.md │ ├── custom-layouts.md │ ├── troubleshooting.md │ └── window-limit.md ├── exportOptions.plist ├── fastlane/ │ ├── Appfile │ ├── Fastfile │ ├── Gymfile │ └── README.md └── privacy-policy.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .amethyst.sample.yml ================================================ # Default settings for Amethyst # Repo: `https://github.com/ianyh/Amethyst` # # Note due to issue 1419 (https://github.com/ianyh/Amethyst/issues/1419) some # config values may conflict and not work if they are the same as the default # values for Amethyst. You can see these values on GitHub at # https://github.com/ianyh/Amethyst/blob/development/Amethyst/default.amethyst # If you're experiencing conflicts and the settings are the same as the default, # comment out the commands in this file. # # Move this file to: `~/.amethyst.yml` # In order to register changes restart Amethyst. # If you experience issues pulling in the changes you can also quit Amethyst and run: `defaults delete com.amethyst.Amethyst.plist` # This removes the current preferences and causes Amethyst to restart with default preferences and pull configs from this file. # layouts - Ordered list of layouts to use by layout key (default tall, wide, fullscreen, and column). layouts: - tall - fullscreen # - tall-right - wide # - two-pane # - middle-wide # - 3column-left # - middle-wide # The legacy name of "3column-middle" # - 3column-right # - 4column-left # - 4column-right - column # - row # - floating # - widescreen-tall # - widescreen-tall-right # - bsp # First mod (default option + shift). mod1: - option - shift # - control # - command # Second mod (default option + shift + control). mod2: - option - shift - control # - command # Commands: # special key values # space # enter # up # right # down # left # special characters require quotes # '.' # ',' # Move to the next layout in the list. cycle-layout: mod: mod1 key: space # Move to the previous layout in the list. cycle-layout-backward: mod: mod2 key: space # Shrink the main pane by a percentage of the screen dimension as defined by window-resize-step. Note that not all layouts respond to this command. shrink-main: mod: mod1 key: h # Expand the main pane by a percentage of the screen dimension as defined by window-resize-step. Note that not all layouts respond to this command. expand-main: mod: mod1 key: l # Increase the number of windows in the main pane. Note that not all layouts respond to this command. increase-main: mod: mod1 key: ',' # Decrease the number of windows in the main pane. Note that not all layouts respond to this command. decrease-main: mod: mod1 key: '.' # General purpose command for custom layouts. Functionality is layout-dependent. # command1: # mod: # key: # General purpose command for custom layouts. Functionality is layout-dependent. # command2: # mod: # key: # General purpose command for custom layouts. Functionality is layout-dependent. # command3: # mod: # key: # General purpose command for custom layouts. Functionality is layout-dependent. # command4: # mod: # key: # Focus the next window in the list going counter-clockwise. focus-ccw: mod: mod1 key: j # Focus the next window in the list going clockwise. focus-cw: mod: mod1 key: k # Focus the main window in the list. focus-main: mod: mod1 key: m # Focus the next screen in the list going counter-clockwise. focus-screen-ccw: mod: mod1 key: p # Focus the next screen in the list going clockwise. focus-screen-cw: mod: mod1 key: n # Move the currently focused window onto the next screen in the list going counter-clockwise. swap-screen-ccw: mod: mod2 key: h # Move the currently focused window onto the next screen in the list going clockwise. swap-screen-cw: mod: mod2 key: l # Swap the position of the currently focused window with the next window in the list going counter-clockwise. swap-ccw: mod: mod2 key: j # Swap the position of the currently focused window with the next window in the list going clockwise. swap-cw: mod: mod2 key: k # Swap the position of the currently focused window with the main window in the list. swap-main: mod: mod1 key: enter # Move focus to the n-th screen in the list; e.g., focus-screen-3 will move mouse focus to the 3rd screen. Note that the main window in the given screen will be focused. #focus-screen-n: # focus-screen-: # mod: mod1 # key: y # Move the currently focused window to the n-th screen; e.g., throw-screen-3 will move the window to the 3rd screen. # throw-screen-n: # throw-screen-: # mod: mod1 # key: u # Move the currently focused window to the n-th space; e.g., throw-space-3 will move the window to the 3rd space. # throw-space-: # mod: mod1 # key: i # Select tall layout select-tall-layout: mod: mod1 key: a # Select wide layout select-wide-layout: mod: mod1 key: s # Select fullscreen layout select-fullscreen-layout: mod: mod1 key: d # Select column layout select-column-layout: mod: mod1 key: f # Move the currently focused window to the space to the left. throw-space-left: mod: mod2 key: left # Move currently the focused window to the space to the right. throw-space-right: mod: mod2 key: right # Toggle the floating state of the currently focused window; i.e., if it was floating make it tiled and if it was tiled make it floating. toggle-float: mod: mod1 key: t # Display the layout HUD with the current layout on each screen. display-current-layout: mod: mod1 key: i # Turn on or off tiling entirely. toggle-tiling: mod: mod2 key: t # Turn on tiling. # enable-tiling: # mod: mod2 # key: # Turn off tiling. # disable-tiling: # mod: mod2 # key: # Rerun the current layout's algorithm. reevaluate-windows: mod: mod1 key: z # Turn on or off focus-follows-mouse. toggle-focus-follows-mouse: mod: mod2 key: x # Automatically quit and reopen Amethyst. relaunch-amethyst: mod: mod2 key: z # disable screen padding on builtin display disable-padding-on-builtin-display: false # Boolean flag for whether or not to add margins between windows (default false). window-margins: false # Boolean flag for whether or not to set window margins if there is only one window on the screen, assuming window margins are enabled (default false). smart-window-margins: false # # Add 10px margin between windows # window-margins: true # window-margin-size: 5 # The size of the margins between windows (in px, default 0). window-margin-size: 0 # The max number of windows that may be visible on a screen at one time before # additional windows are minimized. A value of 0 disables the feature. window-max-count: 0 # The smallest height that a window can be sized to regardless of its layout frame (in px, default 0). window-minimum-height: 0 # The smallest width that a window can be sized to regardless of its layout frame (in px, default 0) window-minimum-width: 0 # List of bundle identifiers for applications to either be automatically floating or automatically tiled based on floating-is-blacklist (default []). floating: [] # Boolean flag determining behavior of the floating list. true if the applications should be floating and all others tiled. false if the applications should be tiled and all others floating (default true). floating-is-blacklist: true # true if screen frames should exclude the status bar. false if the screen frames should include the status bar (default false). ignore-menu-bar: false # true if menu bar icon should be hidden (default false). hide-menu-bar-icon: false # true if windows smaller than the small-window-size threshold should be floating by default (default true) float-small-windows: true # Pixel threshold for float-small-windows. Windows with both width and height below this value are considered small (in px, default 500). small-window-size: 500 # true if the mouse should move position to the center of a window when it becomes focused (default false). Note that this is largely incompatible with focus-follows-mouse. mouse-follows-focus: false # true if the windows underneath the mouse should become focused as the mouse moves (default false). Note that this is largely incompatible with mouse-follows-focus focus-follows-mouse: false # true if dragging and dropping windows on to each other should swap their positions (default false). mouse-swaps-windows: false # true if changing the frame of a window with the mouse should update the layout to accommodate the change (default false). Note that not all layouts will be able to respond to the change. mouse-resizes-windows: false # true to display the name of the layout when a new layout is selected (default true). enables-layout-hud: true # true to display the name of the layout when moving to a new space (default true). enables-layout-hud-on-space-change: true # true to get updates to beta versions of the software (default false). use-canary-build: false # true to insert new windows into the first position and false to insert new windows into the last position (default false). new-windows-to-main: false # true to automatically move to a space when throwing a window to it (default true). follow-space-thrown-windows: true # The integer percentage of the screen dimension to increment and decrement main pane ratios by (default 5). window-resize-step: 5 # Padding to apply between windows and the left edge of the screen (in px, default 0). screen-padding-left: 0 # Padding to apply between windows and the right edge of the screen (in px, default 0). screen-padding-right: 0 # Padding to apply between windows and the top edge of the screen (in px, default 0). screen-padding-top: 0 # Padding to apply between windows and the bottom edge of the screen (in px, default 0). screen-padding-bottom: 0 # true to maintain layout state across application executions (default true). restore-layouts-on-launch: true # true to display some optional debug information in the layout HUD (default false). debug-layout-info: false ================================================ FILE: .eslintrc.yml ================================================ --- env: es6: true parserOptions: ecmaVersion: 9 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Applications:** What applications are involved? **To Reproduce** Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Versions:** - macOS: - Amethyst: **Debug Info** ``` $ /Applications/Amethyst.app/Contents/MacOS/Amethyst --debug-info [--include-apps] ``` Note: `--include-apps` will list your manageable applications, but is optional if you don't want to list that. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches: [ development ] pull_request: jobs: build: name: Build and run unit tests runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Dependencies run: | brew bundle - name: Test run: | set -o pipefail && xcodebuild -workspace Amethyst.xcworkspace -scheme Amethyst clean test | xcbeautify ================================================ FILE: .gitignore ================================================ # Xcode .DS_Store */build/* *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata profile *.moved-aside DerivedData .idea/ *.hmap Pods build crashlytics_api_key crashlytics_app_key Carthage AMKeys.h fastlane/report.xml *profraw # Homebrew Brewfile.lock.json ================================================ FILE: .hound.yml ================================================ swiftlint: config_file: .swiftlint.yml eslint: enabled: true config_file: .eslintrc.yml ================================================ FILE: .swiftlint.yml ================================================ disabled_rules: - function_body_length - closing_brace - statement_position - force_cast - force_try - no_space_in_method_call - file_length - type_body_length included: - Amethyst - AmethystTests line_length: warning: 200 ignores_comments: true cyclomatic_complexity: 15 large_tuple: 3 nesting: type_level: 2 identifier_name: excluded: - id ================================================ FILE: Amethyst/Amethyst-Bridging-Header.h ================================================ ================================================ FILE: Amethyst/Amethyst-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} LSUIElement NSMainNibFile MainMenu NSPrincipalClass NSApplication SUCanaryFeedURL https://ianyh.com/amethyst/canary-appcast.xml SUFeedURL https://ianyh.com/amethyst/appcast.xml ================================================ FILE: Amethyst/Amethyst.entitlements ================================================ ================================================ FILE: Amethyst/AmethystDebug.entitlements ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.disable-library-validation ================================================ FILE: Amethyst/AppDelegate.swift ================================================ // // AppDelegate.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/8/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import CoreServices import Foundation import LoginServiceKit import RxCocoa import RxSwift import Silica import Sparkle import SwiftyBeaver class AppDelegate: NSObject, NSApplicationDelegate { static let windowManagerEncodingKey = "EncodedWindowManager" @IBOutlet var preferencesWindowController: PreferencesWindowController? fileprivate var windowManager: WindowManager? private var hotKeyManager: HotKeyManager? fileprivate var statusItem: NSStatusItem? @IBOutlet var statusItemMenu: NSMenu? @IBOutlet var versionMenuItem: NSMenuItem? @IBOutlet var startAtLoginMenuItem: NSMenuItem? @IBOutlet var toggleGlobalTilingMenuItem: NSMenuItem? @IBOutlet var layoutsMenuItem: NSMenuItem? private var isFirstLaunch = true func applicationDidFinishLaunching(_ notification: Notification) { #if DEBUG log.addDestination(ConsoleDestination()) #endif if CommandLine.arguments.contains("--log") { let destination = ConsoleDestination() destination.useNSLog = true log.addDestination(destination) } log.info("Logging is enabled") log.debug("Debug logging is enabled") UserConfiguration.shared.delegate = self UserConfiguration.shared.load() #if RELEASE let appcastURLString = { () -> String? in if UserConfiguration.shared.useCanaryBuild() { return Bundle.main.infoDictionary?["SUCanaryFeedURL"] as? String } else { return Bundle.main.infoDictionary?["SUFeedURL"] as? String } }()! SUUpdater.shared().feedURL = URL(string: appcastURLString) #endif preferencesWindowController?.window?.level = .floating if let encodedWindowManager = UserDefaults.standard.data(forKey: AppDelegate.windowManagerEncodingKey), UserConfiguration.shared.restoreLayoutsOnLaunch() { let decoder = JSONDecoder() windowManager = try? decoder.decode(WindowManager.self, from: encodedWindowManager) } windowManager = windowManager ?? WindowManager(userConfiguration: UserConfiguration.shared) hotKeyManager = HotKeyManager(userConfiguration: UserConfiguration.shared) hotKeyManager?.setUpWithWindowManager(windowManager!, configuration: UserConfiguration.shared, appDelegate: self) } override func awakeFromNib() { super.awakeFromNib() let version = Bundle.main.infoDictionary?["CFBundleVersion"] as! String let shortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String let statusItemImage = NSImage(named: "icon-statusitem") statusItemImage?.isTemplate = true statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusItem?.image = statusItemImage statusItem?.menu = statusItemMenu statusItem?.highlightMode = true let hideMenuBarIcon: Bool = UserConfiguration.shared.hideMenuBarIcon() statusItem?.isVisible = !hideMenuBarIcon versionMenuItem?.title = "Version \(shortVersion) (\(version))" toggleGlobalTilingMenuItem?.title = "Disable" startAtLoginMenuItem?.state = (LoginServiceKit.isExistLoginItems(at: Bundle.main.bundlePath) ? .on : .off) // Set up status item menu delegate to refresh layouts when main menu is opened statusItemMenu?.delegate = self } func applicationDidBecomeActive(_ notification: Notification) { guard !isFirstLaunch else { isFirstLaunch = false return } showPreferencesWindow(self) } func applicationWillTerminate(_ notification: Notification) { guard let windowManager = windowManager else { return } do { let encoder = JSONEncoder() let encodedWindowManager = try encoder.encode(windowManager) UserDefaults.standard.set(encodedWindowManager, forKey: AppDelegate.windowManagerEncodingKey) } catch { log.error("Failed to encode window manager: \(error)") } } @IBAction func toggleStartAtLogin(_ sender: AnyObject) { if startAtLoginMenuItem?.state == .off { LoginServiceKit.addLoginItems(at: Bundle.main.bundlePath) } else { LoginServiceKit.removeLoginItems(at: Bundle.main.bundlePath) } startAtLoginMenuItem?.state = (LoginServiceKit.isExistLoginItems(at: Bundle.main.bundlePath) ? .on : .off) } @IBAction func toggleGlobalTiling(_ sender: AnyObject) { UserConfiguration.shared.tilingEnabled = !UserConfiguration.shared.tilingEnabled windowManager?.markAllScreensForReflow() } @IBAction func resetLayouts(_ sender: AnyObject) { UserDefaults.standard.removeObject(forKey: AppDelegate.windowManagerEncodingKey) windowManager?.reset() } @IBAction func relaunch(_ sender: AnyObject) { AppManager.relaunch() } @IBAction func showPreferencesWindow(_ sender: AnyObject) { guard let isVisible = preferencesWindowController?.window?.isVisible, !isVisible else { return } preferencesWindowController?.showWindow(nil) NSApp.activate(ignoringOtherApps: true) presentDotfileWarningIfNecessary() } @IBAction func checkForUpdates(_ sender: AnyObject) { #if RELEASE SUUpdater.shared().checkForUpdates(sender) #endif } private func presentDotfileWarningIfNecessary() { let shouldWarn = !UserDefaults.standard.bool(forKey: "disable-dotfile-conflict-warning") if shouldWarn && UserConfiguration.shared.hasCustomConfiguration() { let alert = NSAlert() alert.alertStyle = .warning alert.messageText = "Warning" alert.informativeText = "You have a .amethyst file, which can override in-app preferences. You may encounter unexpected behavior." alert.showsSuppressionButton = true alert.runModal() if alert.suppressionButton?.state == .on { UserDefaults.standard.set(true, forKey: "disable-dotfile-conflict-warning") } } } private func populateLayoutsMenu() { guard let layoutsMenuItem = layoutsMenuItem, let submenu = layoutsMenuItem.submenu else { return } // Clear existing items submenu.removeAllItems() // Get screen manager: try focused screen first, then screen under mouse cursor let screenManager: ScreenManager>? = { if let focused = windowManager?.focusedScreenManager() { return focused } // Fallback to screen containing mouse cursor (useful when clicking menu bar) let mouseLocation = NSEvent.mouseLocation if let nsScreen = NSScreen.screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) }) { let amScreen = AMScreen(screen: nsScreen) return windowManager?.screenManager(for: amScreen) } return nil }() guard let screenManager = screenManager else { let errorItem = NSMenuItem(title: "Unable to determine current screen", action: nil, keyEquivalent: "") errorItem.isEnabled = false submenu.addItem(errorItem) return } // Get layouts from the screen manager (not from global config) let layouts = screenManager.layoutsInfo // Check if no layouts are available and return early if layouts.isEmpty { let noLayoutsItem = NSMenuItem(title: "No layouts enabled", action: nil, keyEquivalent: "") noLayoutsItem.isEnabled = false submenu.addItem(noLayoutsItem) return } // Add menu items for each layout in the screen manager for layoutInfo in layouts { let menuItem = NSMenuItem(title: layoutInfo.name, action: #selector(selectLayout(_:)), keyEquivalent: "") menuItem.target = self menuItem.representedObject = layoutInfo.key menuItem.state = layoutInfo.isSelected ? .on : .off submenu.addItem(menuItem) } } @IBAction func selectLayout(_ sender: NSMenuItem) { guard let layoutKey = sender.representedObject as? String, let windowManager = windowManager, let screenManager = windowManager.focusedScreenManager() else { return } screenManager.selectLayout(layoutKey) // Menu will be refreshed automatically when next opened via NSMenuDelegate } } extension AppDelegate: NSWindowDelegate { func windowWillClose(_ notification: Notification) { windowManager?.preferencesDidClose() } } extension AppDelegate: NSMenuDelegate { func menuWillOpen(_ menu: NSMenu) { // Refresh layouts menu when main status item menu is about to open if menu == statusItemMenu { populateLayoutsMenu() } } } extension AppDelegate: UserConfigurationDelegate { func configurationGlobalTilingDidChange(_ userConfiguration: UserConfiguration) { var statusItemImage: NSImage? if UserConfiguration.shared.tilingEnabled == true { statusItemImage = NSImage(named: "icon-statusitem") toggleGlobalTilingMenuItem?.title = "Disable Tiling" } else { statusItemImage = NSImage(named: "icon-statusitem-disabled") toggleGlobalTilingMenuItem?.title = "Enable Tiling" } statusItemImage?.isTemplate = true statusItem?.image = statusItemImage } func configurationAccessibilityPermissionsDidChange(_ userConfiguration: UserConfiguration) { windowManager?.reevaluateWindows() } } ================================================ FILE: Amethyst/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: Amethyst/Categories/NSRunningApplication+Manageable.swift ================================================ // // NSRunningApplication+Manageable.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/8/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import AppKit import Foundation enum Manageable { case manageable case unmanageable case undetermined } private let ignoredBundleIDs = Set([ "com.apple.dashboard", "com.apple.loginwindow", "com.apple.notificationcenterui", "com.apple.wifi.WiFiAgent", "com.apple.Spotlight", "com.apple.systemuiserver", "com.apple.dock", "com.apple.AirPlayUIAgent", "com.apple.dock.extra", "com.apple.PowerChime", "com.apple.WebKit.Networking", "com.apple.WebKit.WebContent", "com.apple.WebKit.GPU", "com.apple.FollowUpUI", "com.apple.controlcenter", "com.apple.SoftwareUpdateNotificationManager", "com.apple.TextInputMenuAgent", "com.apple.TextInputSwitcher", "com.apple.WindowManager", "com.apple.accessibility.AXVisualSupportAgent", "com.apple.talagent", "com.apple.wallpaper.agent", "com.apple.CharacterPaletteIM", "com.apple.LocalAuthentication.UIAgent", "com.apple.security.Keychain-Circle-Notification", "com.apple.backgroundtaskmanagement.agent", "com.apple.CoreLocationAgent", "com.apple.OSDUIHelper", "com.apple.ViewBridgeAuxiliary" ]) protocol BundleIdentifiable { var bundleIdentifier: String? { get } } extension NSRunningApplication: BundleIdentifiable {} extension NSRunningApplication { var isManageable: Manageable { if let bundleIdentifier = bundleIdentifier, ignoredBundleIDs.contains(bundleIdentifier) { return .unmanageable } guard isFinishedLaunching else { return .undetermined } guard case .regular = activationPolicy else { return .undetermined } return .manageable } } ================================================ FILE: Amethyst/Categories/NSTableView+Amethyst.swift ================================================ // // NSTableView+Amethyst.swift // Amethyst // // Created by James Zaghini on 15/5/18. // Copyright © 2018 Ian Ynda-Hummel. All rights reserved. // import Cocoa extension NSTableView { static let noRowSelectedIndex = -1 } ================================================ FILE: Amethyst/Debug/AppsInfo.swift ================================================ // // AppsInfo.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/9/23. // Copyright © 2023 Ian Ynda-Hummel. All rights reserved. // import ArgumentParser import Cocoa import Silica struct Apps: ParsableCommand { @Flag(help: "Include unmanaged applications.") var includeUnmanaged = false mutating func run() throws { let applications = NSWorkspace.shared.runningApplications for application in applications where includeUnmanaged || application.isManageable == .manageable { let app = SIApplication(runningApplication: application) print(""" Title: \(app.title() ?? "") Bundle Identifier: \(application.bundleIdentifier ?? "") Activation Policy: \(application.activationPolicy) pid: \(app.pid()) Manageable: \(application.isManageable) """) } } } ================================================ FILE: Amethyst/Debug/DebugInfo.swift ================================================ // // DebugInfo.swift // Amethyst // // Created by Ian Ynda-Hummel on 2/24/20. // Copyright © 2020 Ian Ynda-Hummel. All rights reserved. // import ArgumentParser import Cocoa struct Debug: ParsableCommand { static var configuration: CommandConfiguration = CommandConfiguration( abstract: "Generate diagnostic reports on system state.", subcommands: [Apps.self, Windows.self], defaultSubcommand: Windows.self ) } struct DebugInfo { static func description(arguments: [String]) -> String { var infos = [ "Version: \(version())", "OS version: \(ProcessInfo.processInfo.operatingSystemVersionString)", "Screens:\n\(screens())", "Configuration:\n\(config())" ] if arguments.contains("--include-apps") { infos.append("Manageable applications:\n\(applications())") } return infos.joined(separator: "\n\n") } static func version() -> String { let version = Bundle.main.infoDictionary?["CFBundleVersion"] as! String let shortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String return "\(shortVersion) (\(version))" } static func isProcessTrusted() -> Bool { let options = [ kAXTrustedCheckOptionPrompt.takeRetainedValue() as String: false ] return AXIsProcessTrustedWithOptions(options as CFDictionary) } static func screens() -> String { return NSScreen.screens.map { "\t\($0.frame) [\($0.frameIncludingDockAndMenu())]" }.joined(separator: "\n") } static func applications() -> String { return NSWorkspace.shared.runningApplications .filter { $0.isManageable == .manageable } .map { "\t\($0.localizedName ?? "") (\($0.bundleIdentifier ?? ""))" } .joined(separator: "\n") } static func config() -> String { return UserDefaults.standard.dictionaryRepresentation() .filter { ConfigurationKey(rawValue: $0.key) != nil } .map { "\($0): \($1)" }.joined(separator: "\n") } } ================================================ FILE: Amethyst/Debug/ScreensInfo.swift ================================================ // // ScreensInfo.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/9/23. // Copyright © 2023 Ian Ynda-Hummel. All rights reserved. // import ArgumentParser import Cocoa import Silica extension AMScreen { func debugDescription() -> String { return """ \tScreenID: \(screenID()!) \tScreen Frame: \(frame()) """ } } struct Screens: ParsableCommand { mutating func run() throws { } } ================================================ FILE: Amethyst/Debug/WindowsInfo.swift ================================================ // // WindowsInfo.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/5/23. // Copyright © 2023 Ian Ynda-Hummel. All rights reserved. // import ArgumentParser import Cocoa import Foundation import Silica extension AXWindow { func debugInfo(redactTitles: Bool) -> String { let screenDescription = screen().map { AMScreen(screen: $0).debugDescription() } return """ \tTitle: \(redactTitles ? "" : title() ?? "") \tFrame: \(frame()) \tid: \(windowID()) \(screenDescription ?? "Screen: unknown") \tisActive: \(isActive()) \tisOnScreen: \(isOnScreen()) \tisFocused: \(isFocused()) \tshouldBeManaged: \(shouldBeManaged()) \tshouldFloat: \(shouldFloat()) """ } } struct Windows: ParsableCommand { @Flag(help: "Include windows of unmanaged applications.") var includeUnmanaged = false @Flag(help: "Redact window titles.") var redactWindowTitles = false mutating func run() throws { let applications = NSWorkspace.shared.runningApplications for application in applications where includeUnmanaged || application.isManageable == .manageable { let app = SIApplication(runningApplication: application) print("\(app.title() ?? "") (pid \(app.pid()))") for window in app.windows() { let axWindow = AXWindow(element: window)! print(axWindow.debugInfo(redactTitles: redactWindowTitles)) print("") } } } } ================================================ FILE: Amethyst/Events/HotKeyManager.swift ================================================ // // HotKeyManager.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Carbon import Foundation import Silica // Type for defining key code. typealias AMKeyCode = Int // Type for defining modifier flags. typealias AMModifierFlags = NSEvent.ModifierFlags // Specific key code defined to be invalid. // Can be used to identify if a returned key code is valid or not. private let AMKeyCodeInvalid: AMKeyCode = 0xFF typealias HotKeyHandler = () -> Void class HotKeyManager: NSObject { private let userConfiguration: UserConfiguration private(set) lazy var stringToKeyCodes: [String: [AMKeyCode]] = { return self.constructKeyCodeMap() }() init(userConfiguration: UserConfiguration) { self.userConfiguration = userConfiguration super.init() _ = constructKeyCodeMap() } private static func keyCodeForNumber(_ number: NSNumber) -> AMKeyCode { let string = "\(number)" guard !string.isEmpty else { return AMKeyCodeInvalid } switch string.last! { case "1": return kVK_ANSI_1 case "2": return kVK_ANSI_2 case "3": return kVK_ANSI_3 case "4": return kVK_ANSI_4 case "5": return kVK_ANSI_5 case "6": return kVK_ANSI_6 case "7": return kVK_ANSI_7 case "8": return kVK_ANSI_8 case "9": return kVK_ANSI_9 case "0": return kVK_ANSI_0 default: return AMKeyCodeInvalid } } func setUpWithWindowManager(_ windowManager: WindowManager, configuration: UserConfiguration, appDelegate: AppDelegate) { constructCommandWithCommandKey(CommandKey.cycleLayoutForward.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.cycleLayoutForward() } constructCommandWithCommandKey(CommandKey.cycleLayoutBackward.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.cycleLayoutBackward() } constructCommandWithCommandKey(CommandKey.shrinkMain.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.updateCurrentLayout { layout in if let panedLayout = layout as? PanedLayout { panedLayout.shrinkMainPane() } } } constructCommandWithCommandKey(CommandKey.expandMain.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.updateCurrentLayout { layout in if let panedLayout = layout as? PanedLayout { panedLayout.expandMainPane() } } } constructCommandWithCommandKey(CommandKey.increaseMain.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.updateCurrentLayout { layout in if let panedLayout = layout as? PanedLayout { panedLayout.increaseMainPaneCount() } } } constructCommandWithCommandKey(CommandKey.decreaseMain.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.updateCurrentLayout { layout in if let panedLayout = layout as? PanedLayout { panedLayout.decreaseMainPaneCount() } } } constructCommandWithCommandKey(CommandKey.command1.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.updateCurrentLayout { layout in if let customLayout = layout as? CustomLayout { customLayout.command1() } } } constructCommandWithCommandKey(CommandKey.command2.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.updateCurrentLayout { layout in if let customLayout = layout as? CustomLayout { customLayout.command2() } } } constructCommandWithCommandKey(CommandKey.command3.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.updateCurrentLayout { layout in if let customLayout = layout as? CustomLayout { customLayout.command3() } } } constructCommandWithCommandKey(CommandKey.command4.rawValue) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.updateCurrentLayout { layout in if let customLayout = layout as? CustomLayout { customLayout.command4() } } } constructCommandWithCommandKey(CommandKey.focusCCW.rawValue) { windowManager.focusTransitionCoordinator.moveFocusCounterClockwise() } constructCommandWithCommandKey(CommandKey.focusCW.rawValue) { windowManager.focusTransitionCoordinator.moveFocusClockwise() } constructCommandWithCommandKey(CommandKey.focusMain.rawValue) { windowManager.focusTransitionCoordinator.moveFocusToMain() } constructCommandWithCommandKey(CommandKey.focusScreenCCW.rawValue) { windowManager.focusTransitionCoordinator.moveFocusScreenCounterClockwise() } constructCommandWithCommandKey(CommandKey.focusScreenCW.rawValue) { windowManager.focusTransitionCoordinator.moveFocusScreenClockwise() } constructCommandWithCommandKey(CommandKey.swapScreenCCW.rawValue) { windowManager.windowTransitionCoordinator.swapFocusedWindowScreenCounterClockwise() } constructCommandWithCommandKey(CommandKey.swapScreenCW.rawValue) { windowManager.windowTransitionCoordinator.swapFocusedWindowScreenClockwise() } constructCommandWithCommandKey(CommandKey.swapCCW.rawValue) { windowManager.windowTransitionCoordinator.swapFocusedWindowCounterClockwise() } constructCommandWithCommandKey(CommandKey.swapCW.rawValue) { windowManager.windowTransitionCoordinator.swapFocusedWindowClockwise() } constructCommandWithCommandKey(CommandKey.swapMain.rawValue) { windowManager.windowTransitionCoordinator.swapFocusedWindowToMain() } constructCommandWithCommandKey(CommandKey.displayCurrentLayout.rawValue) { DispatchQueue.main.async { windowManager.displayCurrentLayout() } } (1...5).forEach { screenNumber in let focusCommandKey = "\(CommandKey.focusScreenPrefix.rawValue)-\(screenNumber)" let throwCommandKey = "\(CommandKey.throwScreenPrefix.rawValue)-\(screenNumber)" self.constructCommandWithCommandKey(focusCommandKey) { windowManager.focusTransitionCoordinator.focusScreen(at: screenNumber - 1) } self.constructCommandWithCommandKey(throwCommandKey) { windowManager.windowTransitionCoordinator.throwToScreenAtIndex(screenNumber - 1) } } (1...16).forEach { spaceNumber in let commandKey = "\(CommandKey.throwSpacePrefix.rawValue)-\(spaceNumber)" self.constructCommandWithCommandKey(commandKey) { windowManager.windowTransitionCoordinator.pushFocusedWindowToSpace(spaceNumber - 1) } } constructCommandWithCommandKey(CommandKey.throwSpaceLeft.rawValue) { windowManager.windowTransitionCoordinator.pushFocusedWindowToSpaceLeft() } constructCommandWithCommandKey(CommandKey.throwSpaceRight.rawValue) { windowManager.windowTransitionCoordinator.pushFocusedWindowToSpaceRight() } constructCommandWithCommandKey(CommandKey.toggleFloat.rawValue) { windowManager.toggleFloatForFocusedWindow() } constructCommandWithCommandKey(CommandKey.toggleTiling.rawValue) { self.userConfiguration.tilingEnabled = !self.userConfiguration.tilingEnabled windowManager.markAllScreensForReflow() } constructCommandWithCommandKey(CommandKey.enableTiling.rawValue) { guard !self.userConfiguration.tilingEnabled else { return } self.userConfiguration.tilingEnabled = true windowManager.markAllScreensForReflow() } constructCommandWithCommandKey(CommandKey.disableTiling.rawValue) { guard self.userConfiguration.tilingEnabled else { return } self.userConfiguration.tilingEnabled = false windowManager.markAllScreensForReflow() } constructCommandWithCommandKey(CommandKey.reevaluateWindows.rawValue) { windowManager.reevaluateWindows() } constructCommandWithCommandKey(CommandKey.toggleFocusFollowsMouse.rawValue) { self.userConfiguration.toggleFocusFollowsMouse() } constructCommandWithCommandKey(CommandKey.relaunchAmethyst.rawValue) { [weak appDelegate] in appDelegate?.relaunch(self) } constructCommandWithCommandKey(CommandKey.increaseWindowMaxCount.rawValue) { self.userConfiguration.increaseWindowMaxCount() windowManager.markAllScreensForReflow() DispatchQueue.main.async { windowManager.displayWindowCountHUD() } } constructCommandWithCommandKey(CommandKey.decreaseWindowMaxCount.rawValue) { self.userConfiguration.decreaseWindowMaxCount() windowManager.markAllScreensForReflow() DispatchQueue.main.async { windowManager.displayWindowCountHUD() } } LayoutType.availableLayoutStrings().forEach { (layoutKey, _) in self.constructCommandWithCommandKey(UserConfiguration.constructLayoutKeyString(layoutKey)) { let screenManager: ScreenManager>? = windowManager.focusedScreenManager() screenManager?.selectLayout(layoutKey) } } } private func constructKeyCodeMap() -> [String: [AMKeyCode]] { var stringToKeyCodes: [String: [AMKeyCode]] = [:] // Generate unicode character keymapping from keyboard layout data. We go // through all keycodes and create a map of string representations to a list // of key codes. It has to map to a list because a string representation // canmap to multiple codes (e.g., 1 and numpad 1 both have string // representation "1"). var currentKeyboard = TISCopyCurrentKeyboardInputSource().takeRetainedValue() var rawLayoutData = TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData) if rawLayoutData == nil { currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource().takeUnretainedValue() rawLayoutData = TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData) } // Get the layout let layoutData = unsafeBitCast(rawLayoutData, to: CFData.self) let layout: UnsafePointer = unsafeBitCast(CFDataGetBytePtr(layoutData), to: UnsafePointer.self) var keysDown: UInt32 = 0 var chars: [UniChar] = [0, 0, 0, 0] var realLength: Int = 0 for keyCode in (0.. UInt32 { var carbonModifiers: UInt32 = 0 if (modifiers & UInt(NSEvent.ModifierFlags.shift.rawValue)) > 0 { carbonModifiers = carbonModifiers | UInt32(shiftKey) } if (modifiers & UInt(NSEvent.ModifierFlags.command.rawValue)) > 0 { carbonModifiers = carbonModifiers | UInt32(cmdKey) } if (modifiers & UInt(NSEvent.ModifierFlags.option.rawValue)) > 0 { carbonModifiers = carbonModifiers | UInt32(optionKey) } if (modifiers & UInt(NSEvent.ModifierFlags.control.rawValue)) > 0 { carbonModifiers = carbonModifiers | UInt32(controlKey) } return carbonModifiers } static func hotKeyNameToDefaultsKey() -> [[String]] { var hotKeyNameToDefaultsKey: [[String]] = [] hotKeyNameToDefaultsKey.append(["Cycle layout forward", CommandKey.cycleLayoutForward.rawValue]) hotKeyNameToDefaultsKey.append(["Cycle layout backwards", CommandKey.cycleLayoutBackward.rawValue]) hotKeyNameToDefaultsKey.append(["Shrink main pane", CommandKey.shrinkMain.rawValue]) hotKeyNameToDefaultsKey.append(["Expand main pane", CommandKey.expandMain.rawValue]) hotKeyNameToDefaultsKey.append(["Increase main pane count", CommandKey.increaseMain.rawValue]) hotKeyNameToDefaultsKey.append(["Decrease main pane count", CommandKey.decreaseMain.rawValue]) hotKeyNameToDefaultsKey.append(["Increase window max count", CommandKey.increaseWindowMaxCount.rawValue]) hotKeyNameToDefaultsKey.append(["Decrease window max count", CommandKey.decreaseWindowMaxCount.rawValue]) hotKeyNameToDefaultsKey.append(["Custom layout command 1", CommandKey.command1.rawValue]) hotKeyNameToDefaultsKey.append(["Custom layout command 2", CommandKey.command2.rawValue]) hotKeyNameToDefaultsKey.append(["Custom layout command 3", CommandKey.command3.rawValue]) hotKeyNameToDefaultsKey.append(["Custom layout command 4", CommandKey.command4.rawValue]) hotKeyNameToDefaultsKey.append(["Move focus counter clockwise", CommandKey.focusCCW.rawValue]) hotKeyNameToDefaultsKey.append(["Move focus clockwise", CommandKey.focusCW.rawValue]) hotKeyNameToDefaultsKey.append(["Move focus to main window", CommandKey.focusMain.rawValue]) hotKeyNameToDefaultsKey.append(["Move focus to counter clockwise screen", CommandKey.focusScreenCCW.rawValue]) hotKeyNameToDefaultsKey.append(["Move focus to clockwise screen", CommandKey.focusScreenCW.rawValue]) hotKeyNameToDefaultsKey.append(["Swap focused window to counter clockwise screen", CommandKey.swapScreenCCW.rawValue]) hotKeyNameToDefaultsKey.append(["Swap focused window to clockwise screen", CommandKey.swapScreenCW.rawValue]) hotKeyNameToDefaultsKey.append(["Swap focused window counter clockwise", CommandKey.swapCCW.rawValue]) hotKeyNameToDefaultsKey.append(["Swap focused window clockwise", CommandKey.swapCW.rawValue]) hotKeyNameToDefaultsKey.append(["Swap focused window with main window", CommandKey.swapMain.rawValue]) hotKeyNameToDefaultsKey.append(["Force windows to be reevaluated", CommandKey.reevaluateWindows.rawValue]) hotKeyNameToDefaultsKey.append(["Throw focused window to space left", CommandKey.throwSpaceLeft.rawValue]) hotKeyNameToDefaultsKey.append(["Throw focused window to space right", CommandKey.throwSpaceRight.rawValue]) (1...16).forEach { spaceNumber in let name = "Throw focused window to space \(spaceNumber)" hotKeyNameToDefaultsKey.append([name, "\(CommandKey.throwSpacePrefix.rawValue)-\(spaceNumber)"]) } (1...5).forEach { screenNumber in let focusCommandName = "Focus screen \(screenNumber)" let throwCommandName = "Throw focused window to screen \(screenNumber)" let focusCommandKey = "\(CommandKey.focusScreenPrefix.rawValue)-\(screenNumber)" let throwCommandKey = "\(CommandKey.throwScreenPrefix.rawValue)-\(screenNumber)" hotKeyNameToDefaultsKey.append([focusCommandName, focusCommandKey]) hotKeyNameToDefaultsKey.append([throwCommandName, throwCommandKey]) } hotKeyNameToDefaultsKey.append(["Toggle float for focused window", CommandKey.toggleFloat.rawValue]) hotKeyNameToDefaultsKey.append(["Display current layout", CommandKey.displayCurrentLayout.rawValue]) hotKeyNameToDefaultsKey.append(["Toggle focus follows mouse", CommandKey.toggleFocusFollowsMouse.rawValue]) hotKeyNameToDefaultsKey.append(["Toggle global tiling", CommandKey.toggleTiling.rawValue]) hotKeyNameToDefaultsKey.append(["Enable global tiling", CommandKey.enableTiling.rawValue]) hotKeyNameToDefaultsKey.append(["Disable global tiling", CommandKey.disableTiling.rawValue]) for (layoutKey, layoutName) in LayoutType.availableLayoutStrings() { let commandName = "Select \(layoutName) layout" let commandKey = "select-\(layoutKey)-layout" hotKeyNameToDefaultsKey.append([commandName, commandKey]) } hotKeyNameToDefaultsKey.append(["Relaunch Amethyst", CommandKey.relaunchAmethyst.rawValue]) return hotKeyNameToDefaultsKey } } ================================================ FILE: Amethyst/Images.xcassets/123.rectangle.imageset/Contents.json ================================================ { "images" : [ { "filename" : "123.rectangle.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/amethyst.imageset/Contents.json ================================================ { "images" : [ { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x" }, { "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/dots.and.line.vertical.and.cursorarrow.rectangle.imageset/Contents.json ================================================ { "images" : [ { "filename" : "dots.and.line.vertical.and.cursorarrow.rectangle.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/icon-statusitem-disabled.imageset/Contents.json ================================================ { "images" : [ { "filename" : "icon-statusitem-disabled.png", "idiom" : "mac", "scale" : "1x" }, { "filename" : "icon-statusitem-disabled@2x.png", "idiom" : "mac", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/icon-statusitem.imageset/Contents.json ================================================ { "images" : [ { "filename" : "icon-statusitem.png", "idiom" : "mac", "scale" : "1x" }, { "filename" : "icon-statusitem@2x.png", "idiom" : "mac", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/macwindow.on.rectangle.imageset/Contents.json ================================================ { "images" : [ { "filename" : "macwindow.on.rectangle.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/text.and.command.macwindow.imageset/Contents.json ================================================ { "images" : [ { "filename" : "text.and.command.macwindow.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/uiwindow.split.2x1.imageset/Contents.json ================================================ { "images" : [ { "filename" : "uiwindow.split.2x1.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Images.xcassets/waveform.path.ecg.rectangle.imageset/Contents.json ================================================ { "images" : [ { "filename" : "waveform.path.ecg.rectangle.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Amethyst/Layout/Layout.swift ================================================ // // Layout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/3/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica /** A base class for specific layout algorithms defining size and position of windows. - Requires: Specific layouts must subclass and override the following properties and methods: - `layoutName` - `layoutKey` Subclasses can optionally override `layoutDescription` to provide debugging information for the layout state. - Note: Usage of a layout object requires specifying a `WindowType` parameter. */ class Layout: Codable { typealias Screen = Window.Screen private enum CodingKeys: String, CodingKey { case key } /// The display name of the layout. class var layoutName: String { fatalError("Must be implemented by subclass") } /// The configuration key of the layout. class var layoutKey: String { fatalError("Must be implemented by subclass") } /// The display name of the layout. var layoutName: String { return type(of: self).layoutName } /// The configuration key of the layout. var layoutKey: String { return type(of: self).layoutKey } /// The debug description of the layout. var layoutDescription: String { return "" } required init() {} required init(from decoder: Decoder) throws {} /// Base encoder for layouts; basically a noop. func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(layoutKey, forKey: .key) } /** Takes a list of windows and a screen and returns the assignments that would be performed. - Parameters: - windows: The windows to apply the layout algorithm to. - screen: The screen on which those windows should reside. - Returns: The assignments that would be performed given those windows on that screen. */ func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { fatalError("Must be implemented by subclass") } } /// Errors occurring when decoding a layout enum LayoutDecodingError: Error { /** Something about the layout was structurally unsound. Notable example: bsp layout cannot recover if some windows are no longer present, so if we fail to decode a node the layout is no longer sound. */ case invalidLayout } // MARK: Window Querying extension Layout { /** Determines what window the layout would put at a given point. - Parameters: - point: The point to test for location. - windows: The windows to apply the layout algorithm to. - screen: The screen on which those windows should reside. - Returns: The window that the layout would intend to put at `point`. - Note: This does not necessarily correspond to the final position of the window as windows do not necessarily take the exact frame the layout provides. */ func windowAtPoint(_ point: CGPoint, of windowSet: WindowSet, on screen: Screen) -> LayoutWindow? { return frameAssignments(windowSet, on: screen)? .map { $0.frameAssignment } .first { $0.frame.contains(point) }? .window } /** Determines what frame the layout would apply to a given window. - Parameters: - window: The window to test for frame. - windows: The windows to apply the layout algorithm to. - screen: The screen on which those windows should reside. - Returns: The `FrameAssignment` object defining the size and location that the layout would assign to `window`. - Note: This does not necessarily correspond to the final frame of the window as windows do not necessarily take the exact frame the layout provides. */ func assignedFrame(_ window: Window, of windowSet: WindowSet, on screen: Screen) -> FrameAssignment? { guard let assignments = frameAssignments(windowSet, on: screen) else { return nil } return assignments.map { $0.frameAssignment }.first { $0.window.id == window.id() } } } /** A particular kind of layout that organizes windows into a main pane and any number of sub-panes. - Note: The definition is intentionally somewhat layout. This is more intended to demonstrate the expected interface for a fairly common paradigm in Amethyst layouts. */ protocol PanedLayout { /** The ratio of the size of the main pane to the size of the sub-panes. - Requires: The value must be between 0 and 1, inclusive. */ var mainPaneRatio: CGFloat { get } /// The number of windows that make up the main pane. var mainPaneCount: Int { get } /** Takes a direct recommendation for a change in ratio. - Parameters: - rawRatio: The ratio recommended by the caller. - Requires: `rawRatio` must be a valid ratio. - Note: This method should generally be reserved for internal use by the layout. */ func recommendMainPaneRawRatio(rawRatio: CGFloat) /// Reduces the visual footprint of the main pane relative to the sub-panes. func shrinkMainPane() /// Increases the visual footprint of the main pane relative to the sub-panes. func expandMainPane() /// Increases the number of windows that make up the main pane. func increaseMainPaneCount() /// Decreases the number of windows that make up the main pane. func decreaseMainPaneCount() } extension PanedLayout { /// The default debug layout description for paned layouts. It describes the ratio and number of main pane windows. var layoutDescription: String { return "(\(mainPaneRatio), \(mainPaneCount))" } /** Takes a recommendation for a change in ratio, but can modify the ratio to adjust for internal state. - Parameters: - ratio: The ratio recommended by the caller. */ func recommendMainPaneRatio(_ ratio: CGFloat) { guard 0 <= ratio && ratio <= 1 else { log.warning("tried to setMainPaneRatio out of range [0-1]: \(ratio)") return recommendMainPaneRawRatio(rawRatio: max(min(ratio, 1), 0)) } recommendMainPaneRawRatio(rawRatio: ratio) } /// The default behavior of main pane expansion that simply recommends an increase in ratio by the configured resize step. func expandMainPane() { recommendMainPaneRatio(mainPaneRatio + UserConfiguration.shared.windowResizeStep()) } /// The default behavior of main pane shrinking that simply recommends a decrease in ratio by the configured resize step. func shrinkMainPane() { recommendMainPaneRatio(mainPaneRatio - UserConfiguration.shared.windowResizeStep()) } } /** A base class for specific layout algorithms that maintain internal state for defining size and position of windows. - Requires: Specific layouts must subclass and override the following properties and methods: - `updateWithChange(_ windowChange: WindowChange)` - `nextWindowIDCounterClockwise() -> CGWindowID?` - `nextWindowIDClockwise() -> CGWindowID?` Notably, the latter two are necessary for the window manager to determine flow of windows. By default layouts are a simple linear list, but more complex layouts may have different logic. */ class StatefulLayout: Layout { /** Updates internal state of the layout based on a window change. - Parameters: - windowChange: A `WindowChange`. */ func updateWithChange(_ windowChange: Change) { fatalError("Must be implemented by subclass") } /** Determines the window that is before the current window. - Returns: The ID of the window before the current window. */ func nextWindowIDCounterClockwise() -> Window.WindowID? { fatalError("Must be implemented by subclass") } /** Determines the window that is after the current window. - Returns: The ID of the window after the current window. */ func nextWindowIDClockwise() -> Window.WindowID? { fatalError("Must be implemented by subclass") } } ================================================ FILE: Amethyst/Layout/Layouts/BinarySpacePartitioningLayout.swift ================================================ // // BinarySpacePartitioningLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/29/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Silica class TreeNode: Codable { typealias WindowID = Window.WindowID private enum CodingKeys: String, CodingKey { case left case right case windowID } weak var parent: TreeNode? var left: TreeNode? var right: TreeNode? var windowID: WindowID? init() {} required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.left = try values.decodeIfPresent(TreeNode.self, forKey: .left) self.right = try values.decodeIfPresent(TreeNode.self, forKey: .right) self.windowID = try values.decodeIfPresent(WindowID.self, forKey: .windowID) self.left?.parent = self self.right?.parent = self guard valid else { throw LayoutDecodingError.invalidLayout } } var valid: Bool { return (left != nil && right != nil && windowID == nil) || (left == nil && right == nil && windowID != nil) } func findWindowID(_ windowID: WindowID) -> TreeNode? { guard self.windowID == windowID else { return left?.findWindowID(windowID) ?? right?.findWindowID(windowID) } return self } func orderedWindowIDs() -> [WindowID] { guard let windowID = windowID else { let leftWindowIDs = left?.orderedWindowIDs() ?? [] let rightWindowIDs = right?.orderedWindowIDs() ?? [] return leftWindowIDs + rightWindowIDs } return [windowID] } func insertWindowIDAtEnd(_ windowID: WindowID) { guard left == nil && right == nil else { right?.insertWindowIDAtEnd(windowID) return } insertWindowID(windowID) } func insertWindowID(_ windowID: WindowID, atPoint insertionPoint: WindowID) { guard self.windowID == insertionPoint else { left?.insertWindowID(windowID, atPoint: insertionPoint) right?.insertWindowID(windowID, atPoint: insertionPoint) return } insertWindowID(windowID) } func removeWindowID(_ windowID: WindowID) { guard let node = findWindowID(windowID) else { log.error("Trying to remove window not in tree") return } guard let parent = node.parent else { return } guard let grandparent = parent.parent else { if node == parent.left { parent.windowID = parent.right?.windowID } else { parent.windowID = parent.left?.windowID } parent.left = nil parent.right = nil return } if parent == grandparent.left { if node == parent.left { grandparent.left = parent.right } else { grandparent.left = parent.left } grandparent.left?.parent = grandparent } else { if node == parent.left { grandparent.right = parent.right } else { grandparent.right = parent.left } grandparent.right?.parent = grandparent } } func insertWindowID(_ windowID: WindowID) { guard parent != nil || self.windowID != nil else { self.windowID = windowID return } if let parent = parent { let newParent = TreeNode() let newNode = TreeNode() newNode.parent = newParent newNode.windowID = windowID newParent.left = self newParent.right = newNode newParent.parent = parent if self == parent.left { parent.left = newParent } else { parent.right = newParent } self.parent = newParent } else { let newSelf = TreeNode() let newNode = TreeNode() newSelf.windowID = self.windowID self.windowID = nil newNode.windowID = windowID left = newSelf left?.parent = self right = newNode right?.parent = self } } } extension TreeNode: Equatable { static func == (lhs: TreeNode, rhs: TreeNode) -> Bool { return lhs.windowID == rhs.windowID && lhs.left == rhs.left && lhs.right == rhs.right } } class BinarySpacePartitioningLayout: StatefulLayout { typealias WindowID = Window.WindowID private typealias TraversalNode = (node: TreeNode, frame: CGRect) private enum CodingKeys: String, CodingKey { case rootNode } override static var layoutName: String { return "Binary Space Partitioning" } override static var layoutKey: String { return "bsp" } override var layoutDescription: String { return "\(lastKnownFocusedWindowID.debugDescription)" } private var rootNode = TreeNode() private var lastKnownFocusedWindowID: WindowID? required init() { super.init() } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.rootNode = try container.decode(TreeNode.self, forKey: .rootNode) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(rootNode, forKey: .rootNode) } private func constructInitialTreeWithWindows(_ windows: [LayoutWindow]) { for window in windows { guard rootNode.findWindowID(window.id) == nil else { continue } rootNode.insertWindowIDAtEnd(window.id) if window.isFocused { lastKnownFocusedWindowID = window.id } } } override func updateWithChange(_ windowChange: Change) { switch windowChange { case let .add(window): guard rootNode.findWindowID(window.id()) == nil else { log.warning("Trying to add a window already in the tree") return } if let insertionPoint = lastKnownFocusedWindowID, window.id() != insertionPoint, rootNode.findWindowID(insertionPoint) != nil { log.info("insert \(window) - \(window.id()) at point: \(insertionPoint)") rootNode.insertWindowID(window.id(), atPoint: insertionPoint) } else { log.info("insert \(window) - \(window.id()) at end") rootNode.insertWindowIDAtEnd(window.id()) } if window.isFocused() { lastKnownFocusedWindowID = window.id() } case let .remove(window): log.info("remove: \(window) - \(window.id())") rootNode.removeWindowID(window.id()) case let .focusChanged(window): lastKnownFocusedWindowID = window.id() case let .windowSwap(window, otherWindow): let windowID = window.id() let otherWindowID = otherWindow.id() guard let windowNode = rootNode.findWindowID(windowID), let otherWindowNode = rootNode.findWindowID(otherWindowID) else { log.error("Tried to perform an unbalanced window swap: \(windowID) <-> \(otherWindowID)") return } windowNode.windowID = otherWindowID otherWindowNode.windowID = windowID case let .tabChange(window, previousWindow): if rootNode.findWindowID(window.id()) != nil { log.warning("Trying to swap a tab in that is already in the tree: \(window)") rootNode.removeWindowID(window.id()) } guard let previousWindowNode = rootNode.findWindowID(previousWindow.id()) else { log.error("Trying to change tab from a window that is not in the tree: \(previousWindow)") return } if let windowNode = rootNode.findWindowID(window.id()) { log.warning("Trying to swap a tab in from another node") } previousWindowNode.windowID = window.id() case .applicationDeactivate, .applicationActivate, .spaceChange, .layoutChange, .none, .unknown: break } } override func nextWindowIDCounterClockwise() -> WindowID? { guard let focusedWindow = Window.currentlyFocused() else { return nil } let orderedIDs = rootNode.orderedWindowIDs() guard let focusedWindowIndex = orderedIDs.firstIndex(of: focusedWindow.id()) else { return nil } let nextWindowIndex = (focusedWindowIndex == 0 ? orderedIDs.count - 1 : focusedWindowIndex - 1) return orderedIDs[nextWindowIndex] } override func nextWindowIDClockwise() -> WindowID? { guard let focusedWindow = Window.currentlyFocused() else { return nil } let orderedIDs = rootNode.orderedWindowIDs() guard let focusedWindowIndex = orderedIDs.firstIndex(of: focusedWindow.id()) else { return nil } let nextWindowIndex = (focusedWindowIndex == orderedIDs.count - 1 ? 0 : focusedWindowIndex + 1) return orderedIDs[nextWindowIndex] } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } if rootNode.left == nil && rootNode.right == nil { constructInitialTreeWithWindows(windows) } let windowIDMap: [WindowID: LayoutWindow] = windows.reduce([:]) { (windowMap, window) -> [WindowID: LayoutWindow] in var mutableWindowMap = windowMap mutableWindowMap[window.id] = window return mutableWindowMap } let baseFrame = screen.adjustedFrame() var ret: [FrameAssignmentOperation] = [] var traversalNodes: [TraversalNode] = [(node: rootNode, frame: baseFrame)] while !traversalNodes.isEmpty { let traversalNode = traversalNodes[0] traversalNodes = [TraversalNode](traversalNodes.dropFirst(1)) if let windowID = traversalNode.node.windowID { guard let window = windowIDMap[windowID] else { log.warning("Could not find window for ID: \(windowID)") continue } let resizeRules = ResizeRules(isMain: true, unconstrainedDimension: .horizontal, scaleFactor: 1) let frameAssignment = FrameAssignment( frame: traversalNode.frame, window: window, screenFrame: baseFrame, resizeRules: resizeRules ) ret.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) } else { guard let left = traversalNode.node.left, let right = traversalNode.node.right else { log.error("Encountered an invalid node") continue } let frame = traversalNode.frame if frame.width > frame.height { let leftFrame = CGRect( x: frame.origin.x, y: frame.origin.y, width: frame.width / 2.0, height: frame.height ) let rightFrame = CGRect( x: frame.origin.x + frame.width / 2.0, y: frame.origin.y, width: frame.width / 2.0, height: frame.height ) traversalNodes.append((node: left, frame: leftFrame)) traversalNodes.append((node: right, frame: rightFrame)) } else { let topFrame = CGRect( x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height / 2.0 ) let bottomFrame = CGRect( x: frame.origin.x, y: frame.origin.y + frame.height / 2.0, width: frame.width, height: frame.height / 2.0 ) traversalNodes.append((node: left, frame: topFrame)) traversalNodes.append((node: right, frame: bottomFrame)) } } } return ret } } extension BinarySpacePartitioningLayout: Equatable { static func == (lhs: BinarySpacePartitioningLayout, rhs: BinarySpacePartitioningLayout) -> Bool { return lhs.rootNode == rhs.rootNode } } ================================================ FILE: Amethyst/Layout/Layouts/ColumnLayout.swift ================================================ // // ColumnLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/14/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Silica class ColumnLayout: Layout, PanedLayout { override static var layoutName: String { return "Column" } override static var layoutKey: String { return "column" } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() { mainPaneCount += 1 } func decreaseMainPaneCount() { mainPaneCount = max(1, mainPaneCount - 1) } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let mainPaneCount = min(windows.count, self.mainPaneCount) let secondaryPaneCount = windows.count - mainPaneCount let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() let mainPaneWidth = round(screenFrame.width * (hasSecondaryPane ? CGFloat(mainPaneRatio) : 1.0)) let mainPaneWindowWidth = round(mainPaneWidth / CGFloat(mainPaneCount)) let secondaryPaneWindowWidth = hasSecondaryPane ? round((screenFrame.width - mainPaneWidth) / CGFloat(secondaryPaneCount)) : 0.0 return windows.reduce([]) { frameAssignments, window -> [FrameAssignmentOperation] in var assignments = frameAssignments var windowFrame: CGRect = .zero let isMain = frameAssignments.count < mainPaneCount var scaleFactor: CGFloat if isMain { scaleFactor = screenFrame.width / mainPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x + (mainPaneWindowWidth * CGFloat(frameAssignments.count)) windowFrame.origin.y = screenFrame.origin.y windowFrame.size.width = mainPaneWindowWidth windowFrame.size.height = screenFrame.height } else { scaleFactor = (screenFrame.width / secondaryPaneWindowWidth) / CGFloat(secondaryPaneCount) windowFrame.origin.x = screenFrame.origin.x + mainPaneWidth + (secondaryPaneWindowWidth * CGFloat(frameAssignments.count - mainPaneCount)) windowFrame.origin.y = screenFrame.origin.y windowFrame.size.width = secondaryPaneWindowWidth windowFrame.size.height = screenFrame.height } let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) let operation = FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet) assignments.append(operation) return assignments } } } ================================================ FILE: Amethyst/Layout/Layouts/CustomLayout.swift ================================================ // // CustomLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 7/2/21. // Copyright © 2021 Ian Ynda-Hummel. All rights reserved. // import CommonCrypto import Foundation import JavaScriptCore private struct JSWindow { let id: String let window: LayoutWindow } private extension JSValue { func toRoundedRect() -> CGRect { let rect = toRect() return CGRect(x: round(rect.origin.x), y: round(rect.origin.y), width: round(rect.width), height: round(rect.height)) } } private enum LayoutExtension { case none case layout(Layout) } class CustomLayout: StatefulLayout, PanedLayout { typealias WindowID = Window.WindowID private enum CodingKeys: String, CodingKey { case key case fileURL } override static var layoutName: String { return "Custom" } override static var layoutKey: String { return "custom" } override var layoutKey: String { return key } override var layoutName: String { return layout?.objectForKeyedSubscript("name").toString() ?? layoutKey } var mainPaneRatio: CGFloat { return 1.0 } var mainPaneCount: Int { return 1 } private let key: String private let fileURL: URL private lazy var context: JSContext? = { guard let context = JSContext() else { log.error("Failed to create javascript context") return nil } context.exceptionHandler = { (_: JSContext!, value: JSValue!) in let name = value.objectForKeyedSubscript("name").toString() ?? "" let message = value.objectForKeyedSubscript("message").toString() ?? "" let stack = value.objectForKeyedSubscript("stack").toString() ?? "" log.error("\(name): \(message)\n\(stack)") } context.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }") let consoleLog: @convention(block) (String) -> Void = { message in log.debug(message) } context.setObject(unsafeBitCast(consoleLog, to: AnyObject.self), forKeyedSubscript: "_consoleLog" as (NSCopying & NSObjectProtocol)) do { context.evaluateScript(try String(contentsOf: self.fileURL)) } catch { log.error(error) return nil } context.evaluateScript(""" function sanitizeArguments(fn) { return function(...args) { const sanitizedArgs = args.map(arg => !!arg ? JSON.parse(JSON.stringify(arg)) : undefined); return fn(...sanitizedArgs); }; } function normalizedLayout() { const l = layout(); l.getFrameAssignments = sanitizeArguments(l.getFrameAssignments); return l; } """) return context }() private lazy var layout: JSValue? = { return self.context?.objectForKeyedSubscript("normalizedLayout")?.call(withArguments: []) }() private lazy var state: JSValue? = { return self.layout?.objectForKeyedSubscript("initialState") }() private lazy var commands: JSValue? = { return self.layout?.objectForKeyedSubscript("commands") }() private lazy var layoutExtension: LayoutExtension = { guard let extendedLayoutKey = self.layout?.objectForKeyedSubscript("extends"), extendedLayoutKey.isString else { return .none } guard let layout = LayoutType.layoutForKey(extendedLayoutKey.toString()) else { return .none } return .layout(layout) }() required init() { fatalError("must be constructed with a file") } required init(key: String, fileURL: URL) { self.key = key self.fileURL = fileURL super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.key = try values.decode(String.self, forKey: .key) self.fileURL = try values.decode(URL.self, forKey: .fileURL) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(key, forKey: .key) try container.encode(fileURL, forKey: .fileURL) } private func extendedFrameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { switch layoutExtension { case .none: return nil case .layout(let layout): return layout.frameAssignments(windowSet, on: screen) } } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let screenFrame = screen.adjustedFrame() let jsScreenFrameArg = JSValue(rect: screenFrame, in: context)! let jsWindows: [WindowID: JSWindow] = windows.reduce([:]) { partialResult, layoutWindow in let id = idHash(forWindowID: layoutWindow.id) ?? UUID().uuidString let window = JSWindow(id: id, window: layoutWindow) return partialResult.merging([layoutWindow.id: window]) { current, _ in return current } } let jsWindowsArg = windows.map { window -> [String: Any?] in let jsWindow = jsWindows[window.id]! return [ "id": jsWindow.id, "frame": JSValue(rect: jsWindow.window.frame, in: context), "isFocused": jsWindow.window.isFocused ] } let extendedFrames: [[String: Any?]]? = extendedFrameAssignments(windowSet, on: screen)?.compactMap { frameAssignmentOperation in let frameAssignment = frameAssignmentOperation.frameAssignment guard let jsWindow = jsWindows[frameAssignment.window.id] else { return nil } return [ "id": jsWindow.id, "frame": JSValue(rect: frameAssignment.frame, in: context), "isFocused": jsWindow.window.isFocused ] } let args: [Any] = [ jsWindowsArg, jsScreenFrameArg, state ?? JSValue(undefinedIn: context)!, extendedFrames ?? JSValue(undefinedIn: context)! ] guard let getAssignments = layout?.objectForKeyedSubscript("getFrameAssignments"), !getAssignments.isNull && !getAssignments.isUndefined else { return nil } guard let assignments = getAssignments.call(withArguments: args), assignments.isObject else { return nil } return windows.compactMap { window -> FrameAssignmentOperation? in guard let jsWindow = jsWindows[window.id] else { return nil } guard let frame = assignments.objectForKeyedSubscript(jsWindow.id) else { return nil } var unconstrainedDimension: UnconstrainedDimension = .horizontal var scaleFactor = screenFrame.width / frame.toRoundedRect().width if let dimension = frame.objectForKeyedSubscript("unconstrainedDimension")?.toString() { switch dimension { case "horizontal": unconstrainedDimension = .horizontal case "vertical": unconstrainedDimension = .vertical scaleFactor = screenFrame.height / frame.toRoundedRect().height default: log.warning("Encountered unknown unconstrainedDimension value: \(dimension), defaulting to horizontal") unconstrainedDimension = .horizontal } } let isMain = frame.objectForKeyedSubscript("isMain")?.toBool() ?? true let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: unconstrainedDimension, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: frame.toRoundedRect(), window: jsWindow.window, screenFrame: screenFrame, resizeRules: resizeRules ) return FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet) } } override func updateWithChange(_ windowChange: Change) { guard let updateWithChange = layout?.objectForKeyedSubscript("updateWithChange"), !updateWithChange.isNull && !updateWithChange.isUndefined else { return } let updateWithChangeArgs: [Any]? = state.flatMap { state in return [jsChange(forChange: windowChange), state] } guard let updatedState = updateWithChange.call(withArguments: updateWithChangeArgs ?? []), !updatedState.isNull && !updatedState.isUndefined else { log.error("\(layoutKey)): received invalid updated state") return } state = updatedState } func command1() { command(key: "command1") } func command2() { command(key: "command2") } func command3() { command(key: "command3") } func command4() { command(key: "command4") } override func nextWindowIDClockwise() -> Window.WindowID? { return nil } override func nextWindowIDCounterClockwise() -> Window.WindowID? { return nil } private func command(key: String) { guard let command = commands?.objectForKeyedSubscript(key), command.isObject else { log.debug("\(layoutKey) — \(key): no command defined") return } guard let updateState = command.objectForKeyedSubscript("updateState"), !updateState.isNull && !updateState.isUndefined else { log.debug("\(layoutKey) — \(key): no updateState function provided") return } let focusedWindowID = Window.currentlyFocused().flatMap { idHash(forWindowID: $0.id()) } let updateStateArgs: [Any]? = state.flatMap { state in if let id = focusedWindowID { return [state, id] } else { return [state] } } guard let updatedState = updateState.call(withArguments: updateStateArgs ?? []), !updatedState.isNull && !updatedState.isUndefined else { log.error("\(layoutKey) — \(key): received invalid updated state") return } state = updatedState } private func idHash(forWindowID windowID: WindowID) -> String? { do { let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys let encodedID = try encoder.encode(windowID) var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) encodedID.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(encodedID.count), &hash) } return hash.map { String(format: "%02hhx", $0) }.joined() } catch { log.warning("Failed to hash window id: \(error)") return nil } } private func jsChange(forChange change: Change) -> [String: String] { var jsChange: [String: String] = [:] switch change { case .add(window: let window): jsChange["change"] = "add" jsChange["windowID"] = idHash(forWindowID: window.id()) case .remove(window: let window): jsChange["change"] = "remove" jsChange["windowID"] = idHash(forWindowID: window.id()) case .focusChanged(window: let window): jsChange["change"] = "focus_changed" jsChange["windowID"] = idHash(forWindowID: window.id()) case .windowSwap(window: let window, otherWindow: let otherWindow): jsChange["change"] = "window_swap" jsChange["windowID"] = idHash(forWindowID: window.id()) jsChange["otherWindowID"] = idHash(forWindowID: otherWindow.id()) case .applicationActivate: jsChange["change"] = "application_activate" case .applicationDeactivate: jsChange["change"] = "application_deactivate" case .spaceChange: jsChange["change"] = "space_change" case .layoutChange: jsChange["change"] = "layout_change" case .tabChange: jsChange["change"] = "tab_change" case .unknown: jsChange["change"] = "unknown" case .none: jsChange["change"] = "none" } return jsChange } func recommendMainPaneRawRatio(rawRatio: CGFloat) { guard let recommendMainPaneRatio = layout?.objectForKeyedSubscript("recommendMainPaneRatio"), !recommendMainPaneRatio.isNull && !recommendMainPaneRatio.isUndefined else { return } let recommendMainPaneRatioArgs: [Any]? = state.flatMap { [rawRatio, $0] } guard let updatedState = recommendMainPaneRatio.call(withArguments: recommendMainPaneRatioArgs ?? []), !updatedState.isNull && !updatedState.isUndefined else { log.error("\(layoutKey) — recommendMainPaneRawRatio: received invalid updated state") return } state = updatedState } func increaseMainPaneCount() { command(key: "increaseMain") } func decreaseMainPaneCount() { command(key: "decreaseMain") } func shrinkMainPane() { command(key: "shrinkMain") } func expandMainPane() { command(key: "expandMain") } } ================================================ FILE: Amethyst/Layout/Layouts/FloatingLayout.swift ================================================ // // FloatingLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/14/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Silica class FloatingLayout: Layout { override static var layoutName: String { return "Floating" } override static var layoutKey: String { return "floating" } override var layoutDescription: String { return "" } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { return nil } } ================================================ FILE: Amethyst/Layout/Layouts/FourColumnLayout.swift ================================================ // // FourColumnLayout.swift // Amethyst // // Originally created by Ian Ynda-Hummel on 12/15/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // // Modifications by Craig Disselkoen on 09/03/18. // Modifications by Reyk Floeter on 10/28/21. // import Silica // we'd like to hide these structures and enums behind fileprivate, but // https://bugs.swift.org/browse/SR-47 enum FourColumn { case left case middleLeft case middleRight case right } enum FourPane { case main case secondary case tertiary case quaternary } struct FourPaneWidths { var left: CGFloat = 0 var middleLeft: CGFloat = 0 var middleRight: CGFloat = 0 var right: CGFloat = 0 } struct QuadruplePaneArrangement { /// number of windows in pane private let paneCount: [FourPane: UInt] /// height of windows in pane private let paneWindowHeight: [FourPane: CGFloat] /// width of windows in pane private let paneWindowWidth: [FourPane: CGFloat] // how panes relate to columns private let panePosition: [FourPane: FourColumn] /// how columns relate to panes private let columnDesignation: [FourColumn: FourPane] /** - Parameters: - mainPane: which Column is the main Pane - numWindows: how many windows total - numMainPane: how many windows in the main Pane - screenSize: total size of the screen - mainPaneRatio: ratio of the screen taken by main pane */ init(mainPane: FourColumn, numWindows: UInt, numMainPane: UInt, screenSize: CGSize, mainPaneRatio: CGFloat) { // forward and reverse mapping of columns to their designations self.panePosition = { switch mainPane { case .left: return [.main: .left, .secondary: .middleLeft, .tertiary: .middleRight, .quaternary: .right] case .middleLeft: return [.main: .middleLeft, .secondary: .middleRight, .tertiary: .left, .quaternary: .right] case .middleRight: return [.main: .middleRight, .secondary: .middleLeft, .tertiary: .right, .quaternary: .left] case .right: return [.main: .right, .secondary: .middleRight, .tertiary: .middleLeft, .quaternary: .left] } }() // swap keys and values for reverse lookup self.columnDesignation = Dictionary(uniqueKeysWithValues: panePosition.map({ ($1, $0) })) // calculate how many are in each type let mainPaneCount = min(numWindows, numMainPane) let nonMainCount: UInt = numWindows - mainPaneCount // we do tertiary first because a single window produces a zero in integer division by 2 let nonMainPaneCount: UInt = max(nonMainCount / 3, 1) let quaternaryPaneCount = nonMainPaneCount let tertiaryPaneCount = nonMainPaneCount let secondaryPaneCount = nonMainPaneCount + max(nonMainCount, 3) % 3 self.paneCount = [.main: mainPaneCount, .secondary: secondaryPaneCount, .tertiary: tertiaryPaneCount, .quaternary: quaternaryPaneCount] // calculate heights let screenHeight = screenSize.height self.paneWindowHeight = [ .main: round(screenHeight / CGFloat(mainPaneCount)), .secondary: secondaryPaneCount == 0 ? 0.0 : round(screenHeight / CGFloat(secondaryPaneCount)), .tertiary: tertiaryPaneCount == 0 ? 0.0 : round(screenHeight / CGFloat(tertiaryPaneCount)), .quaternary: quaternaryPaneCount == 0 ? 0.0 : round(screenHeight / CGFloat(quaternaryPaneCount)) ] // calculate widths let screenWidth = screenSize.width let mainWindowWidth = round(screenWidth / 4) let nonMainWindowWidth = round(screenWidth / 4) self.paneWindowWidth = [ .main: mainWindowWidth, .secondary: nonMainWindowWidth, .tertiary: nonMainWindowWidth, .quaternary: nonMainWindowWidth ] } func count(_ pane: FourPane) -> UInt { return paneCount[pane]! } func height(_ pane: FourPane) -> CGFloat { return paneWindowHeight[pane]! } func width(_ pane: FourPane) -> CGFloat { return paneWindowWidth[pane]! } func firstIndex(_ pane: FourPane) -> UInt { switch pane { case .main: return 0 case .secondary: return count(.main) case .tertiary: return count(.main) + count(.secondary) case .quaternary: return count(.main) + count(.secondary) + count(.tertiary) } } func pane(ofIndex windowIndex: UInt) -> FourPane { if windowIndex >= firstIndex(.quaternary) { return .quaternary } if windowIndex >= firstIndex(.tertiary) { return .tertiary } if windowIndex >= firstIndex(.secondary) { return .secondary } return .main } /// Given a window index, which Pane does it belong to, and which index within that Pane func coordinates(at windowIndex: UInt) -> (FourPane, UInt) { let pane = self.pane(ofIndex: windowIndex) return (pane, windowIndex - firstIndex(pane)) } /// Get the (height, width) dimensions for a window in the given Pane func windowDimensions(inPane pane: FourPane) -> (CGFloat, CGFloat) { return (height(pane), width(pane)) } /// Get the Column assignment for the given Pane func column(ofPane pane: FourPane) -> FourColumn { return panePosition[pane]! } func pane(ofColumn column: FourColumn) -> FourPane { return columnDesignation[column]! } /// Get the column widths in the order (left, middle, right) func widthsLeftToRight() -> FourPaneWidths { return FourPaneWidths( left: width(pane(ofColumn: .left)), middleLeft: width(pane(ofColumn: .middleLeft)), middleRight: width(pane(ofColumn: .middleRight)), right: width(pane(ofColumn: .right)) ) } } // not an actual Layout, just a base class for the four actual Layouts below class FourColumnLayout: Layout { class var mainColumn: FourColumn { fatalError("Must be implemented by subclass") } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let screenFrame = screen.adjustedFrame() let paneArrangement = QuadruplePaneArrangement( mainPane: type(of: self).mainColumn, numWindows: UInt(windows.count), numMainPane: UInt(mainPaneCount), screenSize: screenFrame.size, mainPaneRatio: mainPaneRatio ) return windows.reduce([]) { frameAssignments, window -> [FrameAssignmentOperation] in var assignments = frameAssignments var windowFrame = CGRect.zero let windowIndex: UInt = UInt(frameAssignments.count) let (pane, paneIndex) = paneArrangement.coordinates(at: windowIndex) let (windowHeight, windowWidth): (CGFloat, CGFloat) = paneArrangement.windowDimensions(inPane: pane) let column: FourColumn = paneArrangement.column(ofPane: pane) let widths = paneArrangement.widthsLeftToRight() let xorigin: CGFloat = screenFrame.origin.x + { switch column { case .left: return 0.0 case .middleLeft: return widths.left case .middleRight: return widths.left + widths.middleLeft case .right: return widths.left + widths.middleLeft + widths.middleRight } }() let scaleFactor: CGFloat = screenFrame.width / { if pane == .main { return paneArrangement.width(.main) } return paneArrangement.width(.secondary) + paneArrangement.width(.tertiary) + paneArrangement.width(.quaternary) }() windowFrame.origin.x = xorigin windowFrame.origin.y = screenFrame.origin.y + (windowHeight * CGFloat(paneIndex)) windowFrame.size.width = windowWidth windowFrame.size.height = windowHeight let isMain = windowIndex < paneArrangement.firstIndex(.secondary) let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) return assignments } } } extension FourColumnLayout { func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() { mainPaneCount += 1 } func decreaseMainPaneCount() { mainPaneCount = max(1, mainPaneCount - 1) } } // implement the two variants class FourColumnLeftLayout: FourColumnLayout, PanedLayout { override static var layoutName: String { return "4Column Left" } override static var layoutKey: String { return "4column-left" } override static var mainColumn: FourColumn { return .middleLeft } } class FourColumnRightLayout: FourColumnLayout, PanedLayout { override static var layoutName: String { return "4Column Right" } override static var layoutKey: String { return "4column-right" } override static var mainColumn: FourColumn { return .middleRight } } ================================================ FILE: Amethyst/Layout/Layouts/FullscreenLayout.swift ================================================ // // FullscreenLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/14/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Silica class FullscreenLayout: Layout { override static var layoutName: String { return "Fullscreen" } override static var layoutKey: String { return "fullscreen" } override var layoutDescription: String { return "" } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let screenFrame = screen.adjustedFrame(disableWindowMargins: UserConfiguration.shared.smartWindowMargins()) return windowSet.windows.map { window in let resizeRules = ResizeRules(isMain: true, unconstrainedDimension: .horizontal, scaleFactor: 1) let frameAssignment = FrameAssignment( frame: screenFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules, disableWindowMargins: UserConfiguration.shared.smartWindowMargins() ) return FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet) } } } ================================================ FILE: Amethyst/Layout/Layouts/RowLayout.swift ================================================ // // RowLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/14/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Silica class RowLayout: Layout, PanedLayout { override static var layoutName: String { return "Row" } override static var layoutKey: String { return "row" } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() { mainPaneCount += 1 } func decreaseMainPaneCount() { mainPaneCount = max(1, mainPaneCount - 1) } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let mainPaneCount = min(windows.count, self.mainPaneCount) let secondaryPaneCount = windows.count - mainPaneCount let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() let mainPaneHeight = floor(screenFrame.size.height * (hasSecondaryPane ? CGFloat(mainPaneRatio) : 1.0)) let mainPaneWindowHeight = floor(mainPaneHeight / CGFloat(mainPaneCount)) let secondaryPaneWindowHeight = hasSecondaryPane ? floor((screenFrame.size.height - mainPaneHeight) / CGFloat(secondaryPaneCount)) : 0.0 return windows.reduce([]) { frameAssignments, window -> [FrameAssignmentOperation] in var assignments = frameAssignments var windowFrame: CGRect = .zero let isMain = frameAssignments.count < mainPaneCount var scaleFactor: CGFloat if isMain { scaleFactor = screenFrame.size.height / mainPaneWindowHeight windowFrame.origin.x = screenFrame.origin.x windowFrame.origin.y = screenFrame.origin.y + (mainPaneWindowHeight * CGFloat(frameAssignments.count)) windowFrame.size.width = screenFrame.width windowFrame.size.height = mainPaneWindowHeight } else { scaleFactor = screenFrame.size.height / secondaryPaneWindowHeight / CGFloat(secondaryPaneCount) windowFrame.origin.x = screenFrame.origin.x windowFrame.origin.y = screenFrame.origin.y + (mainPaneWindowHeight * CGFloat(mainPaneCount)) + (secondaryPaneWindowHeight * CGFloat(frameAssignments.count - mainPaneCount)) windowFrame.size.width = screenFrame.width windowFrame.size.height = secondaryPaneWindowHeight } let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .vertical, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) return assignments } } } ================================================ FILE: Amethyst/Layout/Layouts/TallLayout.swift ================================================ // // TallLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/14/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Silica class TallLayout: Layout, PanedLayout { override static var layoutName: String { return "Tall" } override static var layoutKey: String { return "tall" } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } override var layoutDescription: String { return "" } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() { mainPaneCount += 1 } func decreaseMainPaneCount() { mainPaneCount = max(1, mainPaneCount - 1) } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let mainPaneCount = min(windows.count, self.mainPaneCount) let secondaryPaneCount = windows.count - mainPaneCount let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() let mainPaneWindowHeight = round(screenFrame.size.height / CGFloat(mainPaneCount)) let secondaryPaneWindowHeight = hasSecondaryPane ? round(screenFrame.size.height / CGFloat(secondaryPaneCount)) : 0.0 let mainPaneWindowWidth = round(screenFrame.size.width * (hasSecondaryPane ? CGFloat(mainPaneRatio) : 1.0)) let secondaryPaneWindowWidth = screenFrame.size.width - mainPaneWindowWidth return windows.reduce([]) { acc, window -> [FrameAssignmentOperation] in var assignments = acc var windowFrame = CGRect.zero let isMain = acc.count < mainPaneCount var scaleFactor: CGFloat if isMain { scaleFactor = screenFrame.size.width / mainPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x windowFrame.origin.y = screenFrame.origin.y + (mainPaneWindowHeight * CGFloat(acc.count)) windowFrame.size.width = mainPaneWindowWidth windowFrame.size.height = mainPaneWindowHeight } else { scaleFactor = screenFrame.size.width / secondaryPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x + mainPaneWindowWidth windowFrame.origin.y = screenFrame.origin.y + (secondaryPaneWindowHeight * CGFloat(acc.count - mainPaneCount)) windowFrame.size.width = secondaryPaneWindowWidth windowFrame.size.height = secondaryPaneWindowHeight } let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) return assignments } } } ================================================ FILE: Amethyst/Layout/Layouts/TallRightLayout.swift ================================================ // // TallRightLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/14/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Silica class TallRightLayout: Layout, PanedLayout { override static var layoutName: String { return "Tall Right" } override static var layoutKey: String { return "tall-right" } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() { mainPaneCount += 1 } func decreaseMainPaneCount() { mainPaneCount = max(1, mainPaneCount - 1) } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let mainPaneCount = min(windows.count, self.mainPaneCount) let secondaryPaneCount = windows.count - mainPaneCount let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() let mainPaneWindowHeight = round(screenFrame.size.height / CGFloat(mainPaneCount)) let secondaryPaneWindowHeight = hasSecondaryPane ? round(screenFrame.size.height / CGFloat(secondaryPaneCount)) : 0.0 let secondaryPaneWindowWidth = round(screenFrame.size.width * (hasSecondaryPane ? CGFloat(1.0 - mainPaneRatio) : 0)) let mainPaneWindowWidth = screenFrame.size.width - secondaryPaneWindowWidth return windows.reduce([]) { frameAssignments, window -> [FrameAssignmentOperation] in var assignments = frameAssignments var windowFrame = CGRect.zero let isMain = frameAssignments.count < mainPaneCount var scaleFactor: CGFloat if isMain { scaleFactor = screenFrame.size.width / mainPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x + secondaryPaneWindowWidth windowFrame.origin.y = screenFrame.origin.y + (mainPaneWindowHeight * CGFloat(frameAssignments.count)) windowFrame.size.width = mainPaneWindowWidth windowFrame.size.height = mainPaneWindowHeight } else { scaleFactor = screenFrame.size.width / secondaryPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x windowFrame.origin.y = screenFrame.origin.y + secondaryPaneWindowHeight * CGFloat(windows.count - (frameAssignments.count + 1)) windowFrame.size.width = secondaryPaneWindowWidth windowFrame.size.height = secondaryPaneWindowHeight } let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) return assignments } } } ================================================ FILE: Amethyst/Layout/Layouts/ThreeColumnLayout.swift ================================================ // // ThreeColumnLayout.swift // Amethyst // // Originally created by Ian Ynda-Hummel on 12/15/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // // Modifications by Craig Disselkoen on 09/03/18. // import Silica // we'd like to hide these structures and enums behind fileprivate, but // https://bugs.swift.org/browse/SR-47 enum Column { case left case middle case right } enum Pane { case main case secondary case tertiary } struct TriplePaneArrangement { /// number of windows in pane private let paneCount: [Pane: UInt] /// height of windows in pane private let paneWindowHeight: [Pane: CGFloat] /// width of windows in pane private let paneWindowWidth: [Pane: CGFloat] // how panes relate to columns private let panePosition: [Pane: Column] /// how columns relate to panes private let columnDesignation: [Column: Pane] /** - Parameters: - mainPane: which Column is the main Pane - numWindows: how many windows total - numMainPane: how many windows in the main Pane - screenSize: total size of the screen - mainPaneRatio: ratio of the screen taken by main pane */ init(mainPane: Column, numWindows: UInt, numMainPane: UInt, screenSize: CGSize, mainPaneRatio: CGFloat) { // forward and reverse mapping of columns to their designations self.panePosition = { switch mainPane { case .left: return [.main: .left, .secondary: .middle, .tertiary: .right] case .middle: return [.main: .middle, .secondary: .left, .tertiary: .right] case .right: return [.main: .right, .secondary: .left, .tertiary: .middle] } }() // swap keys and values for reverse lookup self.columnDesignation = Dictionary(uniqueKeysWithValues: panePosition.map({ ($1, $0) })) // calculate how many are in each type let mainPaneCount = min(numWindows, numMainPane) let nonMainCount: UInt = numWindows - mainPaneCount // we do tertiary first because a single window produces a zero in integer division by 2 let tertiaryPaneCount = nonMainCount >> 1 let secondaryPaneCount = nonMainCount - tertiaryPaneCount self.paneCount = [.main: mainPaneCount, .secondary: secondaryPaneCount, .tertiary: tertiaryPaneCount] // calculate heights let screenHeight = screenSize.height self.paneWindowHeight = [ .main: round(screenHeight / CGFloat(mainPaneCount)), .secondary: secondaryPaneCount == 0 ? 0.0 : round(screenHeight / CGFloat(secondaryPaneCount)), .tertiary: tertiaryPaneCount == 0 ? 0.0 : round(screenHeight / CGFloat(tertiaryPaneCount)) ] // calculate widths let screenWidth = screenSize.width let mainWindowWidth = secondaryPaneCount == 0 ? screenWidth : round(screenWidth * mainPaneRatio) let nonMainWindowWidth = round((screenWidth - mainWindowWidth) / 2) self.paneWindowWidth = [ .main: mainWindowWidth, .secondary: nonMainWindowWidth, .tertiary: nonMainWindowWidth ] } func count(_ pane: Pane) -> UInt { return paneCount[pane]! } func height(_ pane: Pane) -> CGFloat { return paneWindowHeight[pane]! } func width(_ pane: Pane) -> CGFloat { return paneWindowWidth[pane]! } func firstIndex(_ pane: Pane) -> UInt { switch pane { case .main: return 0 case .secondary: return count(.main) case .tertiary: return count(.main) + count(.secondary) } } func pane(ofIndex windowIndex: UInt) -> Pane { if windowIndex >= firstIndex(.tertiary) { return .tertiary } if windowIndex >= firstIndex(.secondary) { return .secondary } return .main } /// Given a window index, which Pane does it belong to, and which index within that Pane func coordinates(at windowIndex: UInt) -> (Pane, UInt) { let pane = self.pane(ofIndex: windowIndex) return (pane, windowIndex - firstIndex(pane)) } /// Get the (height, width) dimensions for a window in the given Pane func windowDimensions(inPane pane: Pane) -> (CGFloat, CGFloat) { return (height(pane), width(pane)) } /// Get the Column assignment for the given Pane func column(ofPane pane: Pane) -> Column { return panePosition[pane]! } func pane(ofColumn column: Column) -> Pane { return columnDesignation[column]! } /// Get the column widths in the order (left, middle, right) func widthsLeftToRight() -> (CGFloat, CGFloat, CGFloat) { return (width(pane(ofColumn: .left)), width(pane(ofColumn: .middle)), width(pane(ofColumn: .right))) } } // not an actual Layout, just a base class for the three actual Layouts below class ThreeColumnLayout: Layout { class var mainColumn: Column { fatalError("Must be implemented by subclass") } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let screenFrame = screen.adjustedFrame() let paneArrangement = TriplePaneArrangement( mainPane: type(of: self).mainColumn, numWindows: UInt(windows.count), numMainPane: UInt(mainPaneCount), screenSize: screenFrame.size, mainPaneRatio: mainPaneRatio ) return windows.reduce([]) { frameAssignments, window -> [FrameAssignmentOperation] in var assignments = frameAssignments var windowFrame = CGRect.zero let windowIndex: UInt = UInt(frameAssignments.count) let (pane, paneIndex) = paneArrangement.coordinates(at: windowIndex) let (windowHeight, windowWidth): (CGFloat, CGFloat) = paneArrangement.windowDimensions(inPane: pane) let column: Column = paneArrangement.column(ofPane: pane) let (leftPaneWidth, middlePaneWidth, _): (CGFloat, CGFloat, CGFloat) = paneArrangement.widthsLeftToRight() let xorigin: CGFloat = screenFrame.origin.x + { switch column { case .left: return 0.0 case .middle: return leftPaneWidth case .right: return leftPaneWidth + middlePaneWidth } }() let scaleFactor: CGFloat = screenFrame.width / { if pane == .main { return paneArrangement.width(.main) } return paneArrangement.width(.secondary) + paneArrangement.width(.tertiary) }() windowFrame.origin.x = xorigin windowFrame.origin.y = screenFrame.origin.y + (windowHeight * CGFloat(paneIndex)) windowFrame.size.width = windowWidth windowFrame.size.height = windowHeight let isMain = windowIndex < paneArrangement.firstIndex(.secondary) let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) return assignments } } } extension ThreeColumnLayout { func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() { mainPaneCount += 1 } func decreaseMainPaneCount() { mainPaneCount = max(1, mainPaneCount - 1) } } // implement the three variants class ThreeColumnLeftLayout: ThreeColumnLayout, PanedLayout { override static var layoutName: String { return "3Column Left" } override static var layoutKey: String { return "3column-left" } override static var mainColumn: Column { return .left } } class ThreeColumnMiddleLayout: ThreeColumnLayout, PanedLayout { override static var layoutName: String { return "3Column Middle" } // for backwards compatibility with users who still have 'middle-wide' in their active layouts override static var layoutKey: String { return "middle-wide" } override static var mainColumn: Column { return .middle } } class ThreeColumnRightLayout: ThreeColumnLayout, PanedLayout { override static var layoutName: String { return "3Column Right" } override static var layoutKey: String { return "3column-right" } override static var mainColumn: Column { return .right } } ================================================ FILE: Amethyst/Layout/Layouts/TwoPaneLayout.swift ================================================ // // TwoPaneLayout.swift // Amethyst // // Created by @mwz on 10/06/2021. // Copyright © 2021 Ian Ynda-Hummel. All rights reserved. // import Silica class TwoPaneLayout: Layout, PanedLayout { override static var layoutName: String { return "Two Pane" } override static var layoutKey: String { return "two-pane" } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } override var layoutDescription: String { return "" } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() {} func decreaseMainPaneCount() {} override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let mainPaneCount = min(windows.count, self.mainPaneCount) let secondaryPaneCount = windows.count > 1 ? 1 : 0 let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() let isHorizontal = (screenFrame.size.width / screenFrame.size.height) >= 1 let mainPaneWindowHeight = screenFrame.size.height * (!isHorizontal && hasSecondaryPane ? mainPaneRatio : 1) let secondaryPaneWindowHeight = isHorizontal ? mainPaneWindowHeight : screenFrame.size.height - mainPaneWindowHeight let mainPaneWindowWidth = screenFrame.size.width * (isHorizontal && hasSecondaryPane ? mainPaneRatio : 1) let secondaryPaneWindowWidth = !isHorizontal ? mainPaneWindowWidth : screenFrame.size.width - mainPaneWindowWidth return windows.reduce([]) { acc, window -> [FrameAssignmentOperation] in var assignments = acc var windowFrame = CGRect.zero let isMain = acc.count < mainPaneCount var scaleFactor: CGFloat if isMain { scaleFactor = screenFrame.size.width / mainPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x windowFrame.origin.y = screenFrame.origin.y windowFrame.size.width = mainPaneWindowWidth windowFrame.size.height = mainPaneWindowHeight } else { scaleFactor = screenFrame.size.width / secondaryPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x + (isHorizontal ? mainPaneWindowWidth : 0) windowFrame.origin.y = screenFrame.origin.y + (isHorizontal ? 0 : mainPaneWindowHeight) windowFrame.size.width = secondaryPaneWindowWidth windowFrame.size.height = secondaryPaneWindowHeight } let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) return assignments } } } ================================================ FILE: Amethyst/Layout/Layouts/TwoPaneRightLayout.swift ================================================ // // TwoPaneRightLayout.swift // Amethyst // // Created by Anja on 16.06.23. // Copyright © 2023 Ian Ynda-Hummel. All rights reserved. // import Silica class TwoPaneRightLayout: Layout, PanedLayout { override static var layoutName: String { return "Two Pane Right" } override static var layoutKey: String { return "two-pane-right" } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } override var layoutDescription: String { return "" } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() {} func decreaseMainPaneCount() {} override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let mainPaneCount = min(windows.count, self.mainPaneCount) let secondaryPaneCount = windows.count > 1 ? 1 : 0 let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() let isHorizontal = (screenFrame.size.width / screenFrame.size.height) >= 1 let mainPaneWindowHeight = screenFrame.size.height * (!isHorizontal && hasSecondaryPane ? mainPaneRatio : 1) let secondaryPaneWindowHeight = isHorizontal ? mainPaneWindowHeight : screenFrame.size.height - mainPaneWindowHeight let mainPaneWindowWidth = screenFrame.size.width * (isHorizontal && hasSecondaryPane ? mainPaneRatio : 1) let secondaryPaneWindowWidth = !isHorizontal ? mainPaneWindowWidth : screenFrame.size.width - mainPaneWindowWidth return windows.reduce([]) { acc, window -> [FrameAssignmentOperation] in var assignments = acc var windowFrame = CGRect.zero let isMain = acc.count < mainPaneCount var scaleFactor: CGFloat if isMain { scaleFactor = screenFrame.size.width / mainPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x + (isHorizontal ? secondaryPaneWindowWidth : 0) windowFrame.origin.y = screenFrame.origin.y windowFrame.size.width = mainPaneWindowWidth windowFrame.size.height = mainPaneWindowHeight } else { scaleFactor = screenFrame.size.width / secondaryPaneWindowWidth windowFrame.origin.x = screenFrame.origin.x windowFrame.origin.y = screenFrame.origin.y + (isHorizontal ? 0 : mainPaneWindowHeight) windowFrame.size.width = secondaryPaneWindowWidth windowFrame.size.height = secondaryPaneWindowHeight } let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) return assignments } } } ================================================ FILE: Amethyst/Layout/Layouts/WideLayout.swift ================================================ // // WideLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/14/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Silica class WideLayout: Layout, PanedLayout { override static var layoutName: String { return "Wide" } override static var layoutKey: String { return "wide" } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() { mainPaneCount += 1 } func decreaseMainPaneCount() { mainPaneCount = max(1, mainPaneCount - 1) } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows guard !windows.isEmpty else { return [] } let secondaryPaneCount = windows.count - mainPaneCount let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() let mainPaneWindowHeight = round(screenFrame.height * CGFloat(hasSecondaryPane ? mainPaneRatio : 1)) let secondaryPaneWindowHeight = screenFrame.height - mainPaneWindowHeight let mainPaneWindowWidth = round(screenFrame.width / CGFloat(mainPaneCount)) let secondaryPaneWindowWidth = hasSecondaryPane ? round(screenFrame.width / CGFloat(secondaryPaneCount)) : 0.0 return windows.reduce([]) { frameAssignments, window -> [FrameAssignmentOperation] in var assignments = frameAssignments var windowFrame = CGRect.zero let isMain = frameAssignments.count < mainPaneCount var scaleFactor: CGFloat if isMain { scaleFactor = screenFrame.height / mainPaneWindowHeight windowFrame.origin.x = screenFrame.origin.x + (mainPaneWindowWidth * CGFloat(frameAssignments.count)) windowFrame.origin.y = screenFrame.origin.y windowFrame.size.width = mainPaneWindowWidth windowFrame.size.height = mainPaneWindowHeight } else { scaleFactor = screenFrame.height / secondaryPaneWindowHeight windowFrame.origin.x = screenFrame.origin.x + (secondaryPaneWindowWidth * CGFloat(frameAssignments.count - mainPaneCount)) windowFrame.origin.y = screenFrame.origin.y + mainPaneWindowHeight windowFrame.size.width = secondaryPaneWindowWidth windowFrame.size.height = secondaryPaneWindowHeight } let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .vertical, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) let operation = FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet) assignments.append(operation) return assignments } } } ================================================ FILE: Amethyst/Layout/Layouts/WidescreenTallLayout.swift ================================================ // // WidescreenTallLayout.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/15/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Silica class WidescreenTallLayout: Layout { class var isRight: Bool { fatalError("Must be implemented by subclass") } enum CodingKeys: String, CodingKey { case mainPaneCount case mainPaneRatio } private(set) var mainPaneCount: Int = 1 private(set) var mainPaneRatio: CGFloat = 0.5 required init() { super.init() } required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.mainPaneCount = try values.decode(Int.self, forKey: .mainPaneCount) self.mainPaneRatio = try values.decode(CGFloat.self, forKey: .mainPaneRatio) super.init() } override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(mainPaneCount, forKey: .mainPaneCount) try container.encode(mainPaneRatio, forKey: .mainPaneRatio) } override func frameAssignments(_ windowSet: WindowSet, on screen: Screen) -> [FrameAssignmentOperation]? { let windows = windowSet.windows if windows.count == 0 { return [] } let mainPaneCount = min(windows.count, self.mainPaneCount) let secondaryPaneCount = windows.count - mainPaneCount let hasSecondaryPane = secondaryPaneCount > 0 let screenFrame = screen.adjustedFrame() let mainPaneWindowHeight = screenFrame.height let secondaryPaneWindowHeight = hasSecondaryPane ? round(screenFrame.height / CGFloat(secondaryPaneCount)) : 0.0 let mainPaneWidth = round(screenFrame.size.width * (hasSecondaryPane ? CGFloat(mainPaneRatio) : 1.0)) let mainPaneWindowWidth = round(mainPaneWidth / CGFloat(mainPaneCount)) let secondaryPaneWindowWidth = screenFrame.width - mainPaneWidth return windows.reduce([]) { frameAssignments, window -> [FrameAssignmentOperation] in var assignments = frameAssignments var windowFrame = CGRect.zero let windowIndex = frameAssignments.count let isMain = windowIndex < mainPaneCount let scaleFactor: CGFloat if isMain { scaleFactor = CGFloat(screenFrame.size.width / mainPaneWindowWidth) / CGFloat(mainPaneCount) windowFrame.origin.x = screenFrame.origin.x + mainPaneWindowWidth * CGFloat(windowIndex) if type(of: self).isRight { windowFrame.origin.x += secondaryPaneWindowWidth } windowFrame.origin.y = screenFrame.origin.y windowFrame.size.width = mainPaneWindowWidth windowFrame.size.height = mainPaneWindowHeight } else { scaleFactor = CGFloat(screenFrame.size.width / secondaryPaneWindowWidth) windowFrame.origin.x = screenFrame.origin.x + mainPaneWidth windowFrame.origin.y = screenFrame.origin.y + (secondaryPaneWindowHeight * CGFloat(windowIndex - mainPaneCount)) windowFrame.size.width = secondaryPaneWindowWidth windowFrame.size.height = secondaryPaneWindowHeight if type(of: self).isRight { windowFrame.origin.x = screenFrame.origin.x } } let resizeRules = ResizeRules(isMain: isMain, unconstrainedDimension: .horizontal, scaleFactor: scaleFactor) let frameAssignment = FrameAssignment( frame: windowFrame, window: window, screenFrame: screenFrame, resizeRules: resizeRules ) assignments.append(FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)) return assignments } } } extension WidescreenTallLayout: PanedLayout { func recommendMainPaneRawRatio(rawRatio: CGFloat) { mainPaneRatio = rawRatio } func increaseMainPaneCount() { mainPaneCount += 1 } func decreaseMainPaneCount() { mainPaneCount = max(1, mainPaneCount - 1) } } class WidescreenTallLayoutLeft: WidescreenTallLayout { override class var isRight: Bool { return false } override static var layoutName: String { return "Widescreen Tall" } override static var layoutKey: String { return "widescreen-tall" } } class WidescreenTallLayoutRight: WidescreenTallLayout { override class var isRight: Bool { return true } override static var layoutName: String { return "Widescreen Tall Right" } override static var layoutKey: String { return "widescreen-tall-right" } } ================================================ FILE: Amethyst/Layout/ReflowOperation.swift ================================================ // // ReflowOperation.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/19/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica /// Possible dimensions without constraints. enum UnconstrainedDimension: Int { /// The dimension along the x-axis. case horizontal /// The dimension along the y-axis. case vertical } /** This struct defines what adjustments to a particular window frame are allowed and tracks its size as a proportion of available space (for use in resize calculations). Some window resizes reflect valid adjustments to the frame layout. Some window resizes would not be allowed due to hard constraints. */ struct ResizeRules { /// Whether or not the resize rule is applying to the main frame. let isMain: Bool /// The dimension that is allowed to scale. let unconstrainedDimension: UnconstrainedDimension /// the scale factor for the unconstrained dimension. let scaleFactor: CGFloat /** Determines the new value of the dimension based on the scale factor. Given a new frame, decide which dimension will be honored and return its size. - Parameters: - frame: The frame to transform. - negatePadding: Whether or not to take padding into account. */ func scaledDimension(_ frame: CGRect, negatePadding: Bool) -> CGFloat { let dimension: CGFloat = { switch unconstrainedDimension { case .horizontal: return frame.width case .vertical: return frame.height } }() let padding = UserConfiguration.shared.windowMargins() ? UserConfiguration.shared.windowMarginSize() : 0 return negatePadding ? dimension + padding : dimension } } struct LayoutWindow: Equatable { let id: Window.WindowID let frame: CGRect let isFocused: Bool static func == (lhs: Self, rhs: Self) -> Bool { return lhs.id == rhs.id } } struct WindowSet { let windows: [LayoutWindow] private let isWindowWithIDActive: (Window.WindowID) -> Bool private let isWindowWithIDFloating: (Window.WindowID) -> Bool private let windowForID: (Window.WindowID) -> Window? init( windows: [LayoutWindow], isWindowWithIDActive: @escaping (Window.WindowID) -> Bool, isWindowWithIDFloating: @escaping (Window.WindowID) -> Bool, windowForID: @escaping (Window.WindowID) -> Window? ) { self.windows = windows self.isWindowWithIDActive = isWindowWithIDActive self.isWindowWithIDFloating = isWindowWithIDFloating self.windowForID = windowForID } func isWindowActive(_ window: LayoutWindow) -> Bool { return isWindowWithIDActive(window.id) } func isWindowFloating(_ window: LayoutWindow) -> Bool { return isWindowWithIDFloating(window.id) } func perform(frameAssignment: FrameAssignment) { guard let window = windowForID(frameAssignment.window.id) else { return } guard isWindowWithIDActive(frameAssignment.window.id), !isWindowWithIDFloating(frameAssignment.window.id) else { return } frameAssignment.perform(withWindow: window) } } class FrameAssignmentOperation: Operation { let frameAssignment: FrameAssignment let windowSet: WindowSet init(frameAssignment: FrameAssignment, windowSet: WindowSet) { self.frameAssignment = frameAssignment self.windowSet = windowSet super.init() } override func main() { guard !isCancelled else { return } windowSet.perform(frameAssignment: frameAssignment) } } /// Encapsulation of an assignment of a frame to a window. struct FrameAssignment { /// The frame to apply to the window. let frame: CGRect /// The window that will be moved and sized. let window: LayoutWindow /// The frame of the screen being occupied. let screenFrame: CGRect /// The rules governing constraints to frame transforms let resizeRules: ResizeRules /// If `true`, then window margins won't be applied let disableWindowMargins: Bool init(frame: CGRect, window: LayoutWindow, screenFrame: CGRect, resizeRules: ResizeRules) { self.frame = frame self.window = window self.screenFrame = screenFrame self.resizeRules = resizeRules self.disableWindowMargins = false } init(frame: CGRect, window: LayoutWindow, screenFrame: CGRect, resizeRules: ResizeRules, disableWindowMargins: Bool) { self.frame = frame self.window = window self.screenFrame = screenFrame self.resizeRules = resizeRules self.disableWindowMargins = disableWindowMargins } /// The final frame is the desired frame, but transformed to provide desired padding var finalFrame: CGRect { var ret = frame let padding = floor(UserConfiguration.shared.windowMarginSize() / 2) if UserConfiguration.shared.windowMargins() && !disableWindowMargins { ret.origin.x += padding ret.origin.y += padding ret.size.width -= 2 * padding ret.size.height -= 2 * padding } let windowMinimumWidth = UserConfiguration.shared.windowMinimumWidth() let windowMinimumHeight = UserConfiguration.shared.windowMinimumHeight() if windowMinimumWidth > ret.size.width { ret.origin.x -= ((windowMinimumWidth - ret.size.width) / 2) ret.size.width = windowMinimumWidth } if windowMinimumHeight > ret.size.height { ret.origin.y -= ((windowMinimumHeight - ret.size.height) / 2) ret.size.height = windowMinimumHeight } return ret } /** Given a window frame and based on resizeRules, determine what the main pane ratio would be. This accounts for multiple main windows and primary vs non-primary being resized. - Parameters: - windowFrame: The frame of the window to test ratio against. - Returns: The estimate of the main pane ratio implied by how the frame would be transformed. */ func impliedMainPaneRatio(windowFrame: CGRect) -> CGFloat { let oldDimension = resizeRules.scaledDimension(frame, negatePadding: false) let newDimension = resizeRules.scaledDimension(windowFrame, negatePadding: true) let implied = (newDimension / oldDimension) / resizeRules.scaleFactor return resizeRules.isMain ? implied : 1 - implied } /// Perform the actual application of the frame to the window func perform(withWindow window: Window) { var finalFrame = self.finalFrame var finalOrigin = finalFrame.origin // If this is the focused window then we need to shift it to be on screen regardless of size // We call this "window peeking" (this line here to aid in text search) if window.isFocused() { // Just resize the window first to see what the dimensions end up being // Sometimes applications have internal window requirements that are not exposed to us directly finalFrame.origin = window.frame().origin DispatchQueue.main.sync { window.setFrame(finalFrame, withThreshold: CGSize(width: 1, height: 1)) } // With the real height we can update the frame to account for the current size finalFrame.size = CGSize( width: max(window.frame().width, finalFrame.width), height: max(window.frame().height, finalFrame.height) ) finalOrigin.x = max(screenFrame.minX, min(finalOrigin.x, screenFrame.maxX - finalFrame.size.width)) finalOrigin.y = max(screenFrame.minY, min(finalOrigin.y, screenFrame.maxY - finalFrame.size.height)) } // Move the window to its final frame finalFrame.origin = finalOrigin DispatchQueue.main.sync { window.setFrame(finalFrame, withThreshold: CGSize(width: 1, height: 1)) } } } ================================================ FILE: Amethyst/Managers/AppManager.swift ================================================ // // relaunch.swift // Amethyst // // Created by Agustin Suarez on 2021-02-23. // Copyright © 2021 Ian Ynda-Hummel. All rights reserved. // import Foundation import Cocoa class AppManager { public static func relaunch() { let executablePath = Bundle.main.executablePath! as NSString let fileSystemRepresentedPath = executablePath.fileSystemRepresentation let fileSystemPath = FileManager.default.string(withFileSystemRepresentation: fileSystemRepresentedPath, length: Int(strlen(fileSystemRepresentedPath))) Process.launchedProcess(launchPath: fileSystemPath, arguments: []) NSApp.terminate(self) } } ================================================ FILE: Amethyst/Managers/FocusFollowsMouseManager.swift ================================================ // // FocusFollowsMouseManager.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation import Silica import RxSwift protocol FocusFollowsMouseManagerDelegate: AnyObject { associatedtype Window: WindowType typealias Screen = Window.Screen func windows(onScreen screen: Screen) -> [Window] } class FocusFollowsMouseManager { typealias Window = Delegate.Window typealias Screen = Window.Screen weak var delegate: Delegate? private var lastMouseFocusTime = Date.distantPast private let userConfiguration: UserConfiguration private let disposeBag = DisposeBag() init(userConfiguration: UserConfiguration) { self.userConfiguration = userConfiguration // we want to observe changes to the focusFollowsMouse config, because mouse tracking has CPU cost UserDefaults.standard.rx.observe(Bool.self, ConfigurationKey.focusFollowsMouse.rawValue) .distinctUntilChanged { $0 == $1 } .scan(nil) { [unowned self] existingHandler, followingIsDesired -> Any? in if let handler = existingHandler { NSEvent.removeMonitor(handler) } if followingIsDesired! { return NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { [unowned self] event in self.focusWindowWithMouseMovedEvent(event) } } else { return nil } } .subscribe() .disposed(by: disposeBag) } private func focusWindowWithMouseMovedEvent(_ event: NSEvent) { guard userConfiguration.focusFollowsMouse() else { log.warning("Subscribed to mouse move events that we are ignoring") return } guard let screen = Screen.availableScreens.first(where: { $0.frameIncludingDockAndMenu().contains(event.locationInWindow) }) else { return } guard let windows = delegate?.windows(onScreen: screen) else { return } var mousePoint = NSPointToCGPoint(event.locationInWindow) mousePoint.y = Screen.globalHeight() - mousePoint.y + screen.frameIncludingDockAndMenu().origin.y if let focusedWindow = Window.currentlyFocused() { // If the point is already in the frame of the focused window do nothing. guard !focusedWindow.frame().contains(mousePoint) else { return } } guard let topWindow = WindowsInformation.topWindowForScreenAtPoint(mousePoint, withWindows: windows) else { return } self.lastMouseFocusTime = Date() topWindow.focus() } func recentlyTriggeredFocusFollowsMouse() -> Bool { return Date().timeIntervalSince(lastMouseFocusTime) < 0.5 } } ================================================ FILE: Amethyst/Managers/FocusTransitionCoordinator.swift ================================================ // // FocusTransitionCoordinator.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/24/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation import Silica enum FocusTransition { typealias Screen = Window.Screen case focusWindow(_ window: Window) case focusScreen(_ screen: Screen) } protocol FocusTransitionTarget: AnyObject { associatedtype Application: ApplicationType typealias Window = Application.Window typealias Screen = Window.Screen func executeTransition(_ transition: FocusTransition) func lastFocusedWindow(on screen: Screen) -> Window? func screen(at index: Int) -> Screen? func windows(onScreen screen: Screen) -> [Window] func nextWindowIDClockwise(on screen: Screen) -> Window.WindowID? func nextWindowIDCounterClockwise(on screen: Screen) -> Window.WindowID? func nextScreenIndexClockwise(from screen: Screen) -> Int func nextScreenIndexCounterClockwise(from screen: Screen) -> Int } class FocusTransitionCoordinator { typealias Window = Target.Window typealias Screen = Window.Screen weak var target: Target? private let userConfiguration: UserConfiguration private let focusFollowsMouseManager: FocusFollowsMouseManager> init(userConfiguration: UserConfiguration) { self.userConfiguration = userConfiguration self.focusFollowsMouseManager = FocusFollowsMouseManager(userConfiguration: userConfiguration) self.focusFollowsMouseManager.delegate = self } func moveFocusCounterClockwise() { guard let focusedWindow = Window.currentlyFocused() else { focusScreen(at: 0) return } guard let screen = focusedWindow.screen() else { return } guard let windows = target?.windows(onScreen: screen), !windows.isEmpty else { return } let windowToFocus = { () -> Window in if let nextWindowID = self.target?.nextWindowIDCounterClockwise(on: screen) { let windowToFocusIndex = windows.firstIndex { $0.id() == nextWindowID } ?? 0 return windows[windowToFocusIndex] } else { let windowIndex = windows.firstIndex(of: focusedWindow) ?? 0 let windowToFocusIndex = (windowIndex == 0 ? windows.count - 1 : windowIndex - 1) return windows[windowToFocusIndex] } }() windowToFocus.focus() } func moveFocusClockwise() { guard let focusedWindow = Window.currentlyFocused() else { focusScreen(at: 0) return } guard let screen = focusedWindow.screen() else { return } guard let windows = target?.windows(onScreen: screen), !windows.isEmpty else { return } let windowToFocus = { () -> Window in if let nextWindowID = target?.nextWindowIDClockwise(on: screen) { let windowToFocusIndex = windows.firstIndex { $0.id() == nextWindowID } ?? 0 return windows[windowToFocusIndex] } else { let windowIndex = windows.firstIndex(of: focusedWindow) ?? windows.count - 1 let windowToFocusIndex = (windowIndex + 1) % windows.count return windows[windowToFocusIndex] } }() windowToFocus.focus() } func moveFocusToMain() { guard let focusedWindow = Window.currentlyFocused() else { focusScreen(at: 0) return } guard let screen = focusedWindow.screen() else { return } guard let windows = target?.windows(onScreen: screen), !windows.isEmpty else { return } if focusedWindow.id() == windows[0].id() { (target?.lastFocusedWindow(on: screen) ?? windows[0]).focus() } else { windows[0].focus() } } func focusScreen(at screenIndex: Int) { guard let screen = target?.screen(at: screenIndex) else { return } // Do nothing if the screen is already focused if let focusedWindow = Window.currentlyFocused(), let focusedScreen = focusedWindow.screen(), focusedScreen == screen { return } // If the previous focus has been tracked, then focus the window that had the focus before. if let previouslyFocused = target?.lastFocusedWindow(on: screen), previouslyFocused.isOnScreen() { target?.executeTransition(.focusWindow(previouslyFocused)) return } // If there are no windows on the screen focus the screen directly guard let windows = target?.windows(onScreen: screen), !windows.isEmpty else { target?.executeTransition(.focusScreen(screen)) return } // Otherwise find the topmost window on the screen let screenCenter = NSPointToCGPoint(NSPoint( x: screen.frameIncludingDockAndMenu().midX, y: screen.frameIncludingDockAndMenu().midY )) // If there is no window at that point just focus the screen directly guard let topWindow = WindowsInformation.topWindowForScreenAtPoint(screenCenter, withWindows: windows) ?? windows.first else { target?.executeTransition(.focusScreen(screen)) return } // Otherwise focus the topmost window target?.executeTransition(.focusWindow(topWindow)) } func moveFocusScreenCounterClockwise() { guard let focusedScreen = Window.currentlyFocused()?.screen() else { return } guard let nextScreenIndex = target?.nextScreenIndexCounterClockwise(from: focusedScreen) else { return } focusScreen(at: nextScreenIndex) } func moveFocusScreenClockwise() { guard let focusedScreen = Window.currentlyFocused()?.screen() else { return } guard let screenIndex = target?.nextScreenIndexClockwise(from: focusedScreen) else { return } focusScreen(at: screenIndex) } func recentlyTriggeredFocusFollowsMouse() -> Bool { return focusFollowsMouseManager.recentlyTriggeredFocusFollowsMouse() } } extension FocusTransitionCoordinator: FocusFollowsMouseManagerDelegate { func windows(onScreen screen: Screen) -> [Window] { return target?.windows(onScreen: screen) ?? [] } } ================================================ FILE: Amethyst/Managers/HotKeyRegistrar.swift ================================================ // // HotKeyRegistrar.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Foundation import KeyboardShortcuts import MASShortcut protocol HotKeyRegistrar { func registerHotKey(with string: String?, modifiers: AMModifierFlags?, handler: @escaping () -> Void, defaultsKey: String, override: Bool) } extension HotKeyManager: HotKeyRegistrar { func registerHotKey(with string: String?, modifiers: AMModifierFlags?, handler: @escaping () -> Void, defaultsKey: String, override: Bool) { let name = KeyboardShortcuts.Name(defaultsKey) let migrationKey = "migrated-\(name.rawValue)" let isMigrated = UserDefaults.standard.bool(forKey: migrationKey) defer { UserDefaults.standard.set(true, forKey: migrationKey) KeyboardShortcuts.onKeyUp(for: name, action: handler) } if override { MASShortcutBinder.shared().breakBinding(withDefaultsKey: defaultsKey) UserDefaults.standard.removeObject(forKey: defaultsKey) KeyboardShortcuts.setShortcut(nil, for: name) } guard KeyboardShortcuts.getShortcut(for: name) == nil && (!isMigrated || override) else { return } if let value = UserDefaults.standard.object(forKey: defaultsKey), let shortcut = ValueTransformer(forName: .keyedUnarchiveFromDataTransformerName)?.transformedValue(value) as? MASShortcut { let shortcutKey = KeyboardShortcuts.Key(rawValue: shortcut.keyCode) let newShortcut = KeyboardShortcuts.Shortcut(shortcutKey, modifiers: shortcut.modifierFlags) // Keeping the old shortcuts in defaults for now to prevent data loss // UserDefaults.standard.removeObject(forKey: defaultsKey) KeyboardShortcuts.setShortcut(newShortcut, for: name) return } if let string = string, let modifiers = modifiers { if let keyCodes = stringToKeyCodes[string.lowercased()], !keyCodes.isEmpty { let shortcutKey = KeyboardShortcuts.Key(rawValue: keyCodes[0]) let shortcut = KeyboardShortcuts.Shortcut(shortcutKey, modifiers: modifiers) KeyboardShortcuts.setShortcut(shortcut, for: name) } else { log.warning("String \"\(string)\" does not map to any keycodes") } } } } ================================================ FILE: Amethyst/Managers/LayoutType.swift ================================================ // // LayoutManager.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Foundation extension FileManager { func layoutsDirectory() throws -> URL { let applicationSupportDirectory = try FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) let layoutsDirectory = applicationSupportDirectory .appendingPathComponent("Amethyst", isDirectory: true) .appendingPathComponent("Layouts", isDirectory: true) if !FileManager.default.fileExists(atPath: layoutsDirectory.path, isDirectory: nil) { try FileManager.default.createDirectory( at: layoutsDirectory, withIntermediateDirectories: true, attributes: nil ) } return layoutsDirectory } func layoutFile(key: String) throws -> URL { let layoutsDirectory = try self.layoutsDirectory() return layoutsDirectory.appendingPathComponent("\(key).js") } } enum LayoutType { enum Error: Swift.Error { case unknownLayout } case tall case tallRight case wide case twoPane case twoPaneRight case threeColumnLeft case threeColumnMiddle case threeColumnRight case fourColumnLeft case fourColumnRight case fullscreen case column case row case floating case widescreenTallLeft case widescreenTallRight case binarySpacePartitioning case custom(key: String) static var standardLayouts: [LayoutType] { return [ .tall, .tallRight, .wide, .twoPane, .twoPaneRight, .threeColumnLeft, .threeColumnMiddle, .threeColumnRight, .fourColumnLeft, .fourColumnRight, .fullscreen, .column, .row, .floating, .widescreenTallLeft, .widescreenTallRight, .binarySpacePartitioning ] } var key: String { switch self { case .tall: return "tall" case .tallRight: return "tall-right" case .wide: return "wide" case .twoPane: return "two-pane" case .twoPaneRight: return "two-pane-right" case .threeColumnLeft: return "3column-left" case .threeColumnMiddle: return "middle-wide" case .threeColumnRight: return "3column-right" case .fourColumnLeft: return "4column-left" case .fourColumnRight: return "4column-right" case .fullscreen: return "fullscreen" case .column: return "column" case .row: return "row" case .floating: return "floating" case .widescreenTallLeft: return "widescreen-tall" case .widescreenTallRight: return "widescreen-tall-right" case .binarySpacePartitioning: return "bsp" case .custom(let key): return key } } var layoutClass: Layout.Type { switch self { case .tall: return TallLayout.self case .tallRight: return TallRightLayout.self case .wide: return WideLayout.self case .twoPane: return TwoPaneLayout.self case .twoPaneRight: return TwoPaneRightLayout.self case .threeColumnLeft: return ThreeColumnLeftLayout.self case .threeColumnMiddle: return ThreeColumnMiddleLayout.self case .threeColumnRight: return ThreeColumnRightLayout.self case .fourColumnLeft: return FourColumnLeftLayout.self case .fourColumnRight: return FourColumnRightLayout.self case .fullscreen: return FullscreenLayout.self case .column: return ColumnLayout.self case .row: return RowLayout.self case .floating: return FloatingLayout.self case .widescreenTallLeft: return WidescreenTallLayoutLeft.self case .widescreenTallRight: return WidescreenTallLayoutRight.self case .binarySpacePartitioning: return BinarySpacePartitioningLayout.self case .custom: return CustomLayout.self } } static func from(key: String) -> LayoutType { switch key { case "tall": return .tall case "tall-right": return .tallRight case "wide": return .wide case "two-pane": return .twoPane case "two-pane-right": return .twoPaneRight case "3column-left": return .threeColumnLeft case "middle-wide": return .threeColumnMiddle case "3column-right": return .threeColumnRight case "4column-left": return .fourColumnLeft case "4column-right": return .fourColumnRight case "fullscreen": return .fullscreen case "column": return .column case "row": return .row case "floating": return .floating case "widescreen-tall": return .widescreenTallLeft case "widescreen-tall-right": return .widescreenTallRight case "bsp": return .binarySpacePartitioning default: return .custom(key: key) } } static func layoutForKey(_ layoutKey: String) -> Layout? { let type = LayoutType.from(key: layoutKey) guard case .custom = type else { return type.layoutClass.init() } do { let layoutFile = try FileManager.default.layoutFile(key: layoutKey) guard FileManager.default.fileExists(atPath: layoutFile.path) else { return nil } return CustomLayout(key: layoutKey, fileURL: layoutFile) } catch { return nil } } static func layoutNameForKey(_ layoutKey: String) -> String? { let type = LayoutType.from(key: layoutKey) guard case .custom = type else { return type.layoutClass.layoutName } return layoutForKey(layoutKey)?.layoutName } static func standardLayoutClasses() -> [Layout.Type] { return standardLayouts.compactMap { $0.layoutClass } } // Returns a list of (key, name) pairs static func availableLayoutStrings() -> [(key: String, name: String)] { var layoutTypes = standardLayouts .compactMap { $0.layoutClass } .map { ($0.layoutKey, $0.layoutName) } do { let layoutsDirectory = try FileManager.default.layoutsDirectory() let customLayoutFiles = try FileManager.default.contentsOfDirectory( at: layoutsDirectory, includingPropertiesForKeys: nil, options: [] ).filter { $0.pathExtension == "js" } let customLayouts = customLayoutFiles .map { $0.deletingPathExtension().lastPathComponent } .map { ($0, layoutNameForKey($0)!) } layoutTypes.append(contentsOf: customLayouts) } catch { log.error("failed to parse custom layouts") } return layoutTypes } static func layoutsWithConfiguration(_ userConfiguration: UserConfiguration) -> [Layout] { let layoutKeys: [String] = userConfiguration.layoutKeys() let layouts = layoutKeys.map { layoutKey -> Layout? in guard let layout = LayoutType.layoutForKey(layoutKey) else { log.warning("Unrecognized layout key \(layoutKey)") return nil } return layout } return layouts.compactMap { $0 } } static func encoded(layout: Layout) throws -> Data { return try JSONEncoder().encode(layout) } static func decoded(data: Data, key: String) throws -> Layout { let layoutType = LayoutType.from(key: key) let decoder = JSONDecoder() return try decoder.decode(layoutType.layoutClass, from: data) } } ================================================ FILE: Amethyst/Managers/LogManager.swift ================================================ // // LogManager.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/19/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import SwiftyBeaver let log = SwiftyBeaver.self ================================================ FILE: Amethyst/Managers/ScreenManager.swift ================================================ // // ScreenManager.swift // Amethyst // // Created by Ian Ynda-Hummel on 12/23/15. // Copyright © 2015 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica /// Information about a layout for display in menus struct LayoutMenuItemInfo { let key: String let name: String let isSelected: Bool } protocol ScreenManagerDelegate: AnyObject { associatedtype Window: WindowType func applyWindowLimit(forScreenManager screenManager: ScreenManager, minimizingIn range: (_ windowCount: Int) -> Range) func activeWindowSet(forScreenManager screenManager: ScreenManager) -> WindowSet func onReflowInitiation() func onReflowCompletion() } final class ScreenManager: NSObject, Codable { typealias Window = Delegate.Window typealias Screen = Window.Screen enum CodingKeys: String, CodingKey { case layoutsBySpaceUUID } weak var delegate: Delegate? private(set) var screen: Screen? private(set) var space: Space? /// The last window that has been focused on the screen. This value is updated by the notification observations in /// `ObserveApplicationNotifications`. private(set) var lastFocusedWindow: Window? private let userConfiguration: UserConfiguration private let reflowOperationDispatchQueue = DispatchQueue( label: "ScreenManager.reflowOperationQueue", qos: .utility, attributes: [], autoreleaseFrequency: .inherit, target: nil ) private let reflowOperationQueue = OperationQueue() private var layouts: [Layout] = [] private var currentLayoutIndexBySpaceUUID: [String: Int] = [:] private var layoutsBySpaceUUID: [String: [Layout]] = [:] private var currentLayoutIndex: Int = 0 var previousLayoutKey: String? var currentLayout: Layout? { guard !layouts.isEmpty else { return nil } return layouts[currentLayoutIndex] } /// Returns layout info for all layouts in this screen manager, including selection state var layoutsInfo: [LayoutMenuItemInfo] { return layouts.enumerated().map { index, layout in LayoutMenuItemInfo( key: layout.layoutKey, name: layout.layoutName, isSelected: index == currentLayoutIndex ) } } private let layoutNameWindowController: LayoutNameWindowController init(screen: Screen, delegate: Delegate, userConfiguration: UserConfiguration) { self.screen = screen self.delegate = delegate self.userConfiguration = userConfiguration layoutNameWindowController = LayoutNameWindowController(windowNibName: "LayoutNameWindow") super.init() layouts = LayoutType.layoutsWithConfiguration(userConfiguration) reflowOperationQueue.underlyingQueue = reflowOperationDispatchQueue } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) let layoutsBySpaceUUID = try values.decode([String: [[String: Data]]].self, forKey: .layoutsBySpaceUUID) self.userConfiguration = UserConfiguration.shared self.layoutsBySpaceUUID = try layoutsBySpaceUUID.mapValues { keyedLayouts -> [Layout] in return try ScreenManager.decodedLayouts(from: keyedLayouts, userConfiguration: UserConfiguration.shared) } layoutNameWindowController = LayoutNameWindowController(windowNibName: "LayoutNameWindow") } /** Takes the list of layouts and inserts decoded layouts where appropriate. - Parameters: - encodedLayouts: A list of encoded layouts to be restored. - userConfiguration: User configuration defining the list of layouts. */ static func decodedLayouts(from encodedLayouts: [[String: Data]], userConfiguration: UserConfiguration) throws -> [Layout] { let layouts: [Layout] = LayoutType.layoutsWithConfiguration(userConfiguration) var decodedLayouts: [Layout] = encodedLayouts.compactMap { layout in guard let keyData = layout["key"], let key = String(data: keyData, encoding: .utf8) else { return nil } guard let data = layout["data"] else { return nil } do { return try LayoutType.decoded(data: data, key: key) } catch { log.error("Failed to to decode layout: \(key)") } return nil } // Yes this is quadratic, but if your layout list is long enough for that to be significant what are you even doing? return layouts.map { layout -> Layout in guard let decodedLayoutIndex = decodedLayouts.firstIndex(where: { $0.layoutKey == layout.layoutKey }) else { return layout } return decodedLayouts.remove(at: decodedLayoutIndex) } } func encode(to encoder: Encoder) throws { var values = encoder.container(keyedBy: CodingKeys.self) let layoutsBySpaceUUID = try self.layoutsBySpaceUUID.mapValues { layouts in return try layouts.map { layout -> [String: Data] in let layoutKey = layout.layoutKey.data(using: .utf8)! let encodedLayout = try LayoutType.encoded(layout: layout) return ["key": layoutKey, "data": encodedLayout] } } try values.encode(layoutsBySpaceUUID, forKey: .layoutsBySpaceUUID) } func updateScreen(to screen: Screen) { self.screen = screen } func updateSpace(to space: Space) { if let currentSpace = self.space { currentLayoutIndexBySpaceUUID[currentSpace.uuid] = currentLayoutIndex } self.space = space setCurrentLayoutIndex(currentLayoutIndexBySpaceUUID[space.uuid] ?? 0, changingSpace: true) if let layouts = layoutsBySpaceUUID[space.uuid] { self.layouts = layouts } else { self.layouts = LayoutType.layoutsWithConfiguration(userConfiguration) layoutsBySpaceUUID[space.uuid] = layouts } } func distributeEvent(_ change: Change) { switch change { case let .add(window: window): lastFocusedWindow = window case let .focusChanged(window): lastFocusedWindow = window case let .remove(window): if lastFocusedWindow == window { lastFocusedWindow = nil } case .windowSwap, .applicationActivate, .applicationDeactivate, .spaceChange, .layoutChange, .tabChange, .none, .unknown: break } log.debug("Screen: \(screen?.screenID() ?? "unknown") reflow -- Window Change: \(change)") guard let space, let layouts = layoutsBySpaceUUID[space.uuid] else { log.warning("Trying to distribute an event to a screen with no space") return } for layout in layouts { if let layout = layout as? StatefulLayout { layout.updateWithChange(change) } } } func setNeedsReflow() { reflowOperationQueue.cancelAllOperations() log.debug("Screen: \(screen?.screenID() ?? "unknown") reflow") DispatchQueue.main.async { self.minimizeWindows() self.reflow() } } private func minimizeWindows() { let mainPaneCount = (currentLayout as? PanedLayout)?.mainPaneCount ?? 0 guard UserConfiguration.shared.tilingEnabled, let windowLimit = UserConfiguration.shared.windowMaxCount() else { return } let shouldInsertAtFront = UserConfiguration.shared.sendNewWindowsToMainPane() delegate?.applyWindowLimit(forScreenManager: self, minimizingIn: { windowCount in if windowLimit > windowCount { // Not enough windows to minimize. return 0 ..< 0 } if !(currentLayout is PanedLayout) { // Minimize from the back, for layouts like floating/fullscreen. if shouldInsertAtFront { return windowLimit ..< windowCount } else { return 0 ..< windowCount - windowLimit } } if windowLimit <= mainPaneCount { // Don't minimize main panes. This allowing varying main pane count to pin windows. guard windowCount >= mainPaneCount else {return 0 ..< 0} return mainPaneCount ..< windowCount } // Minimize the oldest non-main panes. if shouldInsertAtFront { return windowLimit ..< windowCount } else { return mainPaneCount ..< windowCount + mainPaneCount - windowLimit } }) } private func reflow() { guard let screen = screen else { return } guard userConfiguration.tilingEnabled, space?.type == CGSSpaceTypeUser else { return } // During rapid Space transitions, activation/focus notifications can arrive before // this screen manager updates its tracked Space. Skip reflow if state is stale. guard let currentSpace = CGSpacesInfo.currentSpaceForScreen(screen), currentSpace.id == space?.id else { return } guard let windows = delegate?.activeWindowSet(forScreenManager: self) else { return } guard let layout = currentLayout, let frameAssignments = layout.frameAssignments(windows, on: screen) else { return } // TODO: fix mff // let mouseFollowsFocus = userConfiguration.mouseFollowsFocus() let completeOperation = BlockOperation() // The complete operation should execute the completion delegate call completeOperation.addExecutionBlock { [unowned completeOperation, weak self] in if completeOperation.isCancelled { return } DispatchQueue.main.async { self?.delegate?.onReflowCompletion() // TODO: fix mff // if mouseFollowsFocus { // if case .windowSwap(let window, _) = event { // window.focus() // } // } } } // The completion should be dependent on all assignments finishing frameAssignments.forEach { completeOperation.addDependency($0) } // Start the operation delegate?.onReflowInitiation() reflowOperationQueue.addOperations(frameAssignments, waitUntilFinished: false) reflowOperationQueue.addOperation(completeOperation) } func updateCurrentLayout(_ updater: (Layout) -> Void) { guard let layout = currentLayout else { return } updater(layout) setNeedsReflow() } func cycleLayoutForward() { setCurrentLayoutIndex((currentLayoutIndex + 1) % layouts.count) setNeedsReflow() } func cycleLayoutBackward() { setCurrentLayoutIndex((currentLayoutIndex == 0 ? layouts.count : currentLayoutIndex) - 1) setNeedsReflow() } func selectLayout(_ layoutString: String) { guard let currentLayoutKey = currentLayout?.layoutKey else { return } let nextLayoutKey = currentLayoutKey == layoutString ? previousLayoutKey : layoutString guard let layoutIndex = layouts.firstIndex(where: { $0.layoutKey == nextLayoutKey }) else { return } setCurrentLayoutIndex(layoutIndex) setNeedsReflow() previousLayoutKey = currentLayoutKey } private func setCurrentLayoutIndex(_ index: Int, changingSpace: Bool = false) { guard (0.. Window.WindowID? { guard let layout = currentLayout as? StatefulLayout else { return nil } return layout.nextWindowIDCounterClockwise() } func nextWindowIDClockwise() -> Window.WindowID? { guard let statefulLayout = currentLayout as? StatefulLayout else { return nil } return statefulLayout.nextWindowIDClockwise() } func displayLayoutHUD() { guard userConfiguration.enablesLayoutHUD(), space?.type == CGSSpaceTypeUser else { return } let currentLayoutName = currentLayout.flatMap({ $0.layoutName }) ?? "None" let currentLayoutDescription = currentLayout?.layoutDescription ?? "" displayCustomHUD(title: currentLayoutName, description: currentLayoutDescription) } @objc func hideLayoutHUD(_ sender: AnyObject) { layoutNameWindowController.close() } func displayCustomHUD(title: String, description: String = "") { guard let screen = screen else { return } guard space?.type == CGSSpaceTypeUser else { return } defer { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideLayoutHUD(_:)), object: nil) perform(#selector(hideLayoutHUD(_:)), with: nil, afterDelay: 0.6) } guard let layoutNameWindow = layoutNameWindowController.window as? LayoutNameWindow else { return } // Use new displayNotification method with dynamic sizing layoutNameWindow.displayNotification(title: title, description: description) // Center the window after resizing let screenFrame = screen.frame() let screenCenter = CGPoint(x: screenFrame.midX, y: screenFrame.midY) let windowOrigin = CGPoint( x: screenCenter.x - layoutNameWindow.frame.width / 2.0, y: screenCenter.y - layoutNameWindow.frame.height / 2.0 ) layoutNameWindow.setFrameOrigin(NSPointFromCGPoint(windowOrigin)) layoutNameWindowController.showWindow(self) } } extension ScreenManager: Comparable { static func < (lhs: ScreenManager, rhs: ScreenManager) -> Bool { guard let lhsScreen = lhs.screen, let rhsScreen = rhs.screen else { return false } let originX1 = lhsScreen.frameWithoutDockOrMenu().origin.x let originX2 = rhsScreen.frameWithoutDockOrMenu().origin.x return originX1 < originX2 } } ================================================ FILE: Amethyst/Managers/Screens.swift ================================================ // // Screens.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/29/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica extension WindowManager { class Screens: Codable { enum CodingKeys: String, CodingKey { case screenManagersCache } private(set) var screenManagers: [ScreenManager>] = [] private var screenManagersCache: [String: ScreenManager>] = [:] init() {} func updateSpaces() { guard let screensInfo = CGScreensInfo() else { return } if Screen.screensHaveSeparateSpaces { for screenDictionary in screensInfo.descriptions { guard let screenID = screenDictionary["Display Identifier"].string else { log.error("Could not identify screen with info: \(screenDictionary)") continue } guard let screenManager = screenManagersCache[screenID] else { log.error("Screen with identifier not managed: \(screenID)") continue } let space = CGSpacesInfo.space(fromScreenDescription: screenDictionary) guard screenManager.space != space else { continue } screenManager.updateSpace(to: space) } } else { for screenManager in screenManagers { let space = CGSpacesInfo.space(fromScreenDescription: screensInfo.descriptions[0]) guard screenManager.space != space else { continue } screenManager.updateSpace(to: space) } } } func focusedScreenManager() -> ScreenManager>? { guard let focusedWindow = Window.currentlyFocused() else { return nil } return screenManagers.first { $0.screen?.screenID() == focusedWindow.screen()?.screenID() } } func updateScreens(windowManager: WindowManager) { var screenManagers: [ScreenManager>] = [] for screen in Screen.availableScreens { guard let screenID = screen.screenID() else { continue } let screenManager = screenManagersCache[screenID] ?? ScreenManager>( screen: screen, delegate: windowManager, userConfiguration: UserConfiguration.shared ) screenManager.delegate = windowManager screenManager.updateScreen(to: screen) screenManagersCache[screenID] = screenManager screenManagers.append(screenManager) } // Window managers are sorted by screen position along the x-axis. // See `ScreenManager`'s `Comparable` conformance self.screenManagers = screenManagers.sorted() updateSpaces() markAllScreensForReflow() } func distributeEventToScreen(_ screen: Screen, change: Change) { screenManagers .filter { $0.screen?.screenID() == screen.screenID() } .forEach { screenManager in screenManager.distributeEvent(change) } } func distributeEventToAllScreens(change: Change) { for screenManager in screenManagers { screenManager.distributeEvent(change) } } func markScreenForReflow(_ screen: Screen) { screenManagers .filter { $0.screen?.screenID() == screen.screenID() } .forEach { screenManager in screenManager.setNeedsReflow() } } func markAllScreensForReflow() { for screenManager in screenManagers { screenManager.setNeedsReflow() } } } } ================================================ FILE: Amethyst/Managers/WindowManager.swift ================================================ // // WindowManager.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/14/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import AppKit import Carbon import Foundation import RxSwift import Silica import SwiftyJSON enum TrackingError: Error { case unreliableFloating case unknownScreen case unknownSpace case alreadyTracked } /** The tolerant interval between the click and the application of a mouse move from focus. - Note: At the time of the check we confirm that the mouse is not _currently_ clicked. However, it is possible that the click happened faster than the focus notification could be processed so that when we process the focus the mouse is no longer clicked. In this case we could incorrectly move the mouse to the center of the focused window. This value is an approximation of the time between a fast click and the focus event being processed. For values larger than this we would expect the mouse to still be clicked. */ private let mouseMoveClickSpeedTolerance: TimeInterval = 0.3 final class WindowManager: NSObject, Codable { typealias Window = Application.Window typealias Screen = Window.Screen struct PendingEvent { let screen: Screen let event: Change } private struct UndeterminedApplication { let application: NSRunningApplication let activationPolicyObservation: NSKeyValueObservation? let isFinishedLaunchingObservation: NSKeyValueObservation? func invalidate() { activationPolicyObservation?.invalidate() isFinishedLaunchingObservation?.invalidate() } } enum CodingKeys: String, CodingKey { case screens } let windowTransitionCoordinator: WindowTransitionCoordinator> let focusTransitionCoordinator: FocusTransitionCoordinator> private var applications: [pid_t: AnyApplication] = [:] private var applicationObservations: [pid_t: UndeterminedApplication] = [:] private var screens: Screens private let windows = Windows() private var lastReflowTime = Date() private var lastFocusDate: Date? private var pendingTabDetection: [Window.WindowID: Window] = [:] private var earlyFocusedWindows: Set = [] private var eventQueue: [PendingEvent] = [] private lazy var mouseStateKeeper = MouseStateKeeper(delegate: self) private lazy var applicationEventHandler = ApplicationEventHandler(delegate: self) private let userConfiguration: UserConfiguration private let disposeBag = DisposeBag() init(userConfiguration: UserConfiguration) { self.userConfiguration = userConfiguration self.screens = Screens() self.windowTransitionCoordinator = WindowTransitionCoordinator>() self.focusTransitionCoordinator = FocusTransitionCoordinator>(userConfiguration: userConfiguration) super.init() initialize() } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.screens = try values.decode(Screens.self, forKey: .screens) self.userConfiguration = UserConfiguration.shared self.windowTransitionCoordinator = WindowTransitionCoordinator>() self.focusTransitionCoordinator = FocusTransitionCoordinator>(userConfiguration: userConfiguration) super.init() initialize() } private func initialize() { windowTransitionCoordinator.target = self focusTransitionCoordinator.target = self addWorkspaceNotificationObserver(NSWorkspace.didHideApplicationNotification, selector: #selector(applicationDidHide(_:))) addWorkspaceNotificationObserver(NSWorkspace.didUnhideApplicationNotification, selector: #selector(applicationDidUnhide(_:))) addWorkspaceNotificationObserver(NSWorkspace.activeSpaceDidChangeNotification, selector: #selector(activeSpaceDidChange(_:))) NotificationCenter.default.addObserver( self, selector: #selector(screenParametersDidChange(_:)), name: NSApplication.didChangeScreenParametersNotification, object: nil ) installApplicationMonitor() reevaluateWindows() screens.updateScreens(windowManager: self) } deinit { NSWorkspace.shared.notificationCenter.removeObserver(self) NotificationCenter.default.removeObserver(self) } func reset() { screens = Screens() reevaluateWindows() screens.updateScreens(windowManager: self) } private func addWorkspaceNotificationObserver(_ name: NSNotification.Name, selector: Selector) { let workspaceNotificationCenter = NSWorkspace.shared.notificationCenter workspaceNotificationCenter.addObserver(self, selector: selector, name: name, object: nil) } @objc func applicationActivated(_ sender: AnyObject) { guard let focusedWindow = Window.currentlyFocused(), let screen = focusedWindow.screen() else { return } markScreenForReflow(screen) // doMouseFollowsFocus(focusedWindow: focusedWindow) } @objc func applicationDidLaunch(_ notification: Notification) { guard let launchedApplication = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } add(runningApplication: launchedApplication) } @objc func applicationDidTerminate(_ notification: Notification) { guard let terminatedApplication = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } guard let application = applicationWithPID(terminatedApplication.processIdentifier) else { return } remove(application: application) } @objc func applicationDidHide(_ notification: Notification) { guard let hiddenApplication = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } guard let application = applicationWithPID(hiddenApplication.processIdentifier) else { return } deactivate(application: application) } @objc func applicationDidUnhide(_ notification: Notification) { guard let unhiddenApplication = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return } guard let application = applicationWithPID(unhiddenApplication.processIdentifier) else { return } application.dropWindowsCache() for window in application.windows() { add(window: window) } activate(application: application) } @objc func activeSpaceDidChange(_ notification: Notification) { // Update spaces across screens so that events get distributed to the correct layouts screens.updateSpaces() for pendingEvent in eventQueue { distributeEventToScreen(pendingEvent.screen, change: pendingEvent.event) } eventQueue.removeAll() pendingTabDetection.removeAll() earlyFocusedWindows.removeAll() for runningApplication in NSWorkspace.shared.runningApplications { let pid = runningApplication.processIdentifier guard let application = applicationWithPID(pid) else { continue } application.dropWindowsCache() for window in application.windows() { add(window: window) } } windows.regenerateActiveIDCache() markAllScreensForReflow() } @objc func screenParametersDidChange(_ notification: Notification) { screens.updateScreens(windowManager: self) } } extension WindowManager: ApplicationEventHandlerDelegate { private func installApplicationMonitor() { let target = GetApplicationEventTarget() let launchedEventSpec = EventTypeSpec(eventClass: OSType(kEventClassApplication), eventKind: OSType(kEventAppLaunched)) let terminatedEventSpec = EventTypeSpec(eventClass: OSType(kEventClassApplication), eventKind: OSType(kEventAppTerminated)) var eventSpecs = [launchedEventSpec, terminatedEventSpec] let eventHandler = UnsafeMutableRawPointer(Unmanaged.passUnretained(applicationEventHandler).toOpaque()) let error = InstallEventHandler(target, applicationEventHandlerUPP, 2, &eventSpecs, eventHandler, nil) if error != noErr { log.error("error installing app launch monitor: \(error)") } } func add(applicationWithPID pid: pid_t) { guard let runningApplication = NSRunningApplication(processIdentifier: pid) else { log.warning("process launched with no application: \(pid)") return } add(runningApplication: runningApplication) } func remove(applicationWithPID pid: pid_t) { guard let application = applicationWithPID(pid) else { log.warning("process terminated with no application: \(pid)") return } remove(application: application) } } extension WindowManager { func preferencesDidClose() { DispatchQueue.main.async { self.focusTransitionCoordinator.focusScreen(at: 0) } } func focusedScreenManager() -> ScreenManager>? { return screens.focusedScreenManager() } fileprivate func applicationWithPID(_ pid: pid_t) -> AnyApplication? { return applications[pid] } fileprivate func add(application: AnyApplication) { guard applications[application.pid()] == nil else { for window in application.windows() { add(window: window) } return } ApplicationObservation(application: application, delegate: self) .addObservers() .subscribe( onCompleted: { [weak self] in self?.applications[application.pid()] = application for window in application.windows() { self?.add(window: window) } } ) .disposed(by: disposeBag) } fileprivate func remove(application: AnyApplication) { for window in application.windows() { remove(window: window) } applications.removeValue(forKey: application.pid()) } fileprivate func activate(application: AnyApplication) { windows.activateApplication(withPID: application.pid()) windows.regenerateActiveIDCache() markAllScreensForReflow() } fileprivate func deactivate(application: AnyApplication) { windows.deactivateApplication(withPID: application.pid()) markAllScreensForReflow() } fileprivate func remove(window: Window) { log.debug("Removing window: \(window)") pendingTabDetection.removeValue(forKey: window.id()) earlyFocusedWindows.remove(window.id()) distributeEventToAllScreens(.remove(window: window)) markAllScreensForReflow() windows.regenerateActiveIDCache() windows.remove(window: window) } func toggleFloatForFocusedWindow() { guard let focusedWindow = Window.currentlyFocused(), let screen = focusedWindow.screen() else { return } guard windows.windows(onScreen: screen).contains(focusedWindow) else { let windowChange: Change = .add(window: focusedWindow) add(window: focusedWindow) guard windows.window(withID: focusedWindow.id()) != nil else { return } windows.setFloating(false, forWindow: focusedWindow) distributeEventToScreen(screen, change: windowChange) markScreenForReflow(screen) return } let windowChange: Change = windows.isWindowFloating(focusedWindow) ? .add(window: focusedWindow) : .remove(window: focusedWindow) windows.setFloating(!windows.isWindowFloating(focusedWindow), forWindow: focusedWindow) distributeEventToScreen(screen, change: windowChange) markScreenForReflow(screen) } func distributeEventToScreen(_ screen: Screen, change: Change) { screens.distributeEventToScreen(screen, change: change) } func distributeEventToAllScreens(_ change: Change) { screens.distributeEventToAllScreens(change: change) } func markScreenForReflow(_ screen: Screen) { screens.markScreenForReflow(screen) } func markAllScreensForReflow() { screens.markAllScreensForReflow() } func displayCurrentLayout() { for screenManager in screens.screenManagers { screenManager.displayLayoutHUD() } } func displayWindowCountHUD() { guard userConfiguration.enablesWindowCountHUD() else { return } for screenManager in screens.screenManagers { let currentCount = userConfiguration.windowMaxCount() ?? 0 let countText = currentCount == 0 ? "Unlimited" : "\(currentCount)" let title = "Window Max Count: \(countText)" screenManager.displayCustomHUD(title: title) } } func add(runningApplication: NSRunningApplication) { switch runningApplication.isManageable { case .manageable: let application = AnyApplication(Application(runningApplication: runningApplication)) add(application: application) case .undetermined: monitorUndeterminedApplication(runningApplication) case .unmanageable: break } } func monitorUndeterminedApplication(_ runningApplication: NSRunningApplication) { let pid = runningApplication.processIdentifier if let previousApplication = applicationObservations[pid] { previousApplication.invalidate() applicationObservations.removeValue(forKey: pid) } let activationPolicyObservation = runningApplication.observe(\.activationPolicy) { [weak self] runningApplication, change in guard case .setting = change.kind else { return } if runningApplication.activationPolicy == .regular { self?.applicationObservations[runningApplication.processIdentifier]?.invalidate() self?.applicationObservations.removeValue(forKey: runningApplication.processIdentifier) self?.add(runningApplication: runningApplication) } } let isFinishedLaunchingObservation = runningApplication.observe(\.isFinishedLaunching) { [weak self] runningApplication, change in guard case .setting = change.kind else { return } if runningApplication.isFinishedLaunching { self?.applicationObservations[runningApplication.processIdentifier]?.invalidate() self?.applicationObservations.removeValue(forKey: runningApplication.processIdentifier) self?.add(runningApplication: runningApplication) } } applicationObservations[pid] = UndeterminedApplication( application: runningApplication, activationPolicyObservation: activationPolicyObservation, isFinishedLaunchingObservation: isFinishedLaunchingObservation ) } func reevaluateWindows() { for runningApplication in NSWorkspace.shared.runningApplications { add(runningApplication: runningApplication) } markAllScreensForReflow() } private func add(window: Window, afterWindow otherWindow: Window? = nil) { log.debug("Adding window: \(window)") guard window.shouldBeManaged() else { log.debug("Window should not be managed: \(window)") return } guard let application = applicationWithPID(window.pid()) else { log.error("Tried to add a window without an application: \(window)") return } defer { windows.regenerateActiveIDCache() } guard !windows.isWindowTracked(window) else { log.debug("Window was already tracked: \(window)") return } ApplicationObservation(application: application, delegate: self) .addObserversForWindow(window) .map { try self.determineFloatForWindow(window, application: application, force: false) } .retry { error in error.enumerated().flatMap { count, error -> Observable in guard error is TrackingError, count < 6 else { return .error(error) } log.debug("error in determining float for window: \(window) - \(error)") return .timer(.milliseconds((count ^ 2 * 100)), scheduler: MainScheduler.instance) } } .catch { error in guard error is TrackingError else { throw error } log.debug("forcing float for window: \(window)") try self.determineFloatForWindow(window, application: application, force: true) return .just(()) } .map { try self.track(window: window, application: application, afterWindow: otherWindow) } .retry { error in error.enumerated().flatMap { count, error -> Observable in guard error is TrackingError, count < 6 else { return .error(error) } log.debug("encountered an error trying to track window: \(error)") return .timer(.milliseconds((count ^ 2 * 100)), scheduler: MainScheduler.instance) } } .subscribe() .disposed(by: disposeBag) } private func determineFloatForWindow(_ window: Window, application: AnyApplication, force: Bool) throws { switch application.defaultFloatForWindow(window) { case .unreliable where !force: throw TrackingError.unreliableFloating case .reliable(.floating), .unreliable(.floating): windows.setFloating(true, forWindow: window) case .reliable(.notFloating), .unreliable(.notFloating): windows.setFloating(false, forWindow: window) } } private func track(window: Window, application: AnyApplication, afterWindow otherWindow: Window? = nil) throws { guard !windows.isWindowTracked(window) else { log.warning("Trying to track a window that is already tracked: \(window)") throw TrackingError.alreadyTracked } guard let screen = window.screen() else { throw TrackingError.unknownScreen } guard CGWindowsInfo.windowSpace(window) != nil else { throw TrackingError.unknownSpace } if let otherWindow = otherWindow { _ = windows.replace(window: window, withWindow: otherWindow) distributeEventToScreen(screen, change: .tabChange(window: window, previousWindow: otherWindow)) } else { windows.add(window: window, atFront: userConfiguration.sendNewWindowsToMainPane()) // Only send .add to layouts if the window is on the currently active space. // Windows tracked during a space change for a different space should not // generate .add events — doing so gives layouts stale data for windows // that aren't visible on the current space. let windowSpace = CGWindowsInfo.windowSpace(window) let currentSpaceID = CGSpacesInfo.currentSpaceForScreen(screen)?.id let isOnCurrentSpace: Bool if let currentSpaceID, let windowSpace { isOnCurrentSpace = currentSpaceID == windowSpace } else { isOnCurrentSpace = true } if isOnCurrentSpace { let windowChange: Change = windows.isWindowFloating(window) ? .unknown : .add(window: window) distributeEventToScreen(screen, change: windowChange) } } markScreenForReflow(screen) } /** This function is a best effort to detect changes between native macOS tabs. - Description: Each "tab" is an independent window, but the underlying system relates them in some way that we do not have access to. The heuristic is to find a window from the same application that has recently left the screen, and swap them. This performs pretty well in steady state, but can be a bit wonky when finding the existing tabs depending on how quick the transitions are. - Parameters: - window: the window that might be a tab change. */ func swapInTab(window: Window) { guard let screen = window.screen() else { return } // We do this to avoid triggering tab swapping when just switching focus between apps. // If the window's app is not running by this point then it's not a tab switch. guard let runningApp = NSRunningApplication(processIdentifier: window.pid()), runningApp.isActive else { return } // We take the windows that are being tracked so we can properly detect when a tab switch is a new tab. // It is important here to compute isActive and isOnScreen as soon as possible for improved accuracy. let applicationWindows = windows.windows(forApplicationWithPID: window.pid()) .map { ($0, windows.isWindowActive($0), $0.isOnScreen()) } var string = "\n\tNew Window: \(window)" applicationWindows.forEach { string += "\n\tExisting window: \($0)" } log.debug(string) for (existingWindow, isActive, isOnScreen) in applicationWindows { guard existingWindow != window else { log.debug("Windows are the same:\n\tNew: \(window)\n\t\(existingWindow)") continue } // The window needs to have been active _at some point_, but must not be currently on screen. let didLeaveScreen = (isActive || windows.isWindowActive(existingWindow)) && !existingWindow.isOnScreen() let isInvalid = existingWindow.cgID() == kCGNullWindowID log.debug(""" Considering window: \(existingWindow) isActive: \(isActive), isOnScreen: \(isOnScreen), isInvalid: \(isInvalid), managed: \(existingWindow.shouldBeManaged()) Recomputed isActive: \(windows.isWindowActive(existingWindow)), isOnScreen: \(existingWindow.isOnScreen()) """) // The window needs to have either left the screen and therefore is being replaced // or be invalid and therefore being removed and can be replaced. guard didLeaveScreen || isInvalid else { log.debug("Window candidate discarded: \(existingWindow)") continue } // We have to make sure that we haven't had a focus change too recently as that could mean // the window is already active, but just became focused by swapping window focus. // The time is in seconds, and too long a time ends up with quick switches triggering tabs to incorrectly // swap. let changeInterval = lastFocusDate.flatMap { abs($0.timeIntervalSinceNow) } if let changeInterval = changeInterval, abs(changeInterval) < 0.1 && !isInvalid { log.debug(""" Window candidate discarded: \(existingWindow) lastFocusChange: \(lastFocusDate?.description ?? "nil") now: \(changeInterval) isInvalid: \(isInvalid) """) continue } log.debug("Selected existing window: \(existingWindow)") guard windows.isWindowTracked(window) else { // If the window isn't tracked we add it in relation to the existing one. pendingTabDetection.removeValue(forKey: window.id()) earlyFocusedWindows.remove(window.id()) add(window: window, afterWindow: existingWindow) return } // If we get here, we are working with a window that has been previously added. // Instead of going through the whole add process, we can just swap the windows in order. pendingTabDetection.removeValue(forKey: window.id()) earlyFocusedWindows.remove(window.id()) windows.replace(window: existingWindow, withWindow: window) windows.regenerateActiveIDCache() // Note that the existing window moving out of screen will be tracked as a remove, // but the "adding" happens above, so we need to distribute the relevant change. distributeEventToScreen(screen, change: .tabChange(window: window, previousWindow: existingWindow)) markScreenForReflow(screen) return } windows.regenerateActiveIDCache() if earlyFocusedWindows.remove(window.id()) != nil { // Focus notification already fired before we got here — the visual // transition is settled so call completeTabDetection directly. completeTabDetection(for: window, on: screen) } else { pendingTabDetection[window.id()] = window } } private func completeTabDetection(for window: Window, on screen: Screen) { windows.regenerateActiveIDCache() let applicationWindows = windows.windows(forApplicationWithPID: window.pid()) for existingWindow in applicationWindows { guard existingWindow != window else { continue } let didLeaveScreen = windows.isWindowActive(existingWindow) && !existingWindow.isOnScreen() let isInvalid = existingWindow.cgID() == kCGNullWindowID guard didLeaveScreen || isInvalid else { continue } log.debug("completeTabDetection: selected candidate \(existingWindow) for \(window)") guard windows.isWindowTracked(window) else { pendingTabDetection.removeValue(forKey: window.id()) earlyFocusedWindows.remove(window.id()) add(window: window, afterWindow: existingWindow) return } pendingTabDetection.removeValue(forKey: window.id()) earlyFocusedWindows.remove(window.id()) windows.replace(window: existingWindow, withWindow: window) windows.regenerateActiveIDCache() distributeEventToScreen(screen, change: .tabChange(window: window, previousWindow: existingWindow)) markScreenForReflow(screen) return } log.debug("completeTabDetection: no candidate found") add(window: window) } func onReflowInitiation() { mouseStateKeeper.handleReflowEvent() } func onReflowCompletion() { // if let focusedWindow = Window.currentlyFocused() { // doMouseFollowsFocus(focusedWindow: focusedWindow) // } // This handler will be executed by the Operation, in a queue. Although async // (and although the docs say that it executes in a separate thread), I consider // this to be thread safe, at least safe enough, because we always want the // latest time that a reflow took place. mouseStateKeeper.handleReflowEvent() lastReflowTime = Date() } func doMouseFollowsFocus(focusedWindow: Window) { guard UserConfiguration.shared.mouseFollowsFocus() else { return } guard NSEvent.pressedMouseButtons == 0 else { // If a mouse button is pressed, then the user is probably dragging something between windows. Do not move the mouse. return } // See the description of mouseMoveClickSpeedTolerance for details. if let interval = mouseStateKeeper.lastClick?.timeIntervalSinceNow, abs(interval) < mouseMoveClickSpeedTolerance { return } if focusTransitionCoordinator.recentlyTriggeredFocusFollowsMouse() { // If we have recently triggered focus-follows-mouse, then disable mouse-follows-focus. Otherwise, the moment // focus-follows-mouse is triggered, the mouse will jump to the center of the focused window. return } let windowFrame = focusedWindow.frame() let mouseCursorPoint = NSPoint(x: windowFrame.midX, y: windowFrame.midY) if let mouseMoveEvent = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: mouseCursorPoint, mouseButton: .left) { mouseMoveEvent.flags = CGEventFlags(rawValue: 0) mouseMoveEvent.post(tap: CGEventTapLocation.cghidEventTap) } } } extension WindowManager: MouseStateKeeperDelegate { func recommendMainPaneRatio(_ ratio: CGFloat) { guard let screenManager: ScreenManager> = focusedScreenManager() else { return } screenManager.updateCurrentLayout { layout in if let panedLayout = layout as? PanedLayout { panedLayout.recommendMainPaneRatio(ratio) } } } func swapDraggedWindowWithDropzone(_ draggedWindow: Window) { guard let screen = draggedWindow.screen() else { return } let windows: [Window] = self.windows.windows(onScreen: screen) // need to flip mouse coordinate system to fit Amethyst https://stackoverflow.com/a/45289010/2063546 let flippedPointerLocation = NSPointToCGPoint(NSEvent.mouseLocation) let unflippedY = Screen.globalHeight() - flippedPointerLocation.y + screen.frameIncludingDockAndMenu().origin.y let pointerLocation = NSPointToCGPoint(NSPoint(x: flippedPointerLocation.x, y: unflippedY)) if let screenManager: ScreenManager> = focusedScreenManager(), let layout = screenManager.currentLayout { let windowSet = self.windows.windowSet(forWindowsOnScreen: screen) if let layoutWindow = layout.windowAtPoint(pointerLocation, of: windowSet, on: screen), let framedWindow = self.windows.window(withID: layoutWindow.id) { executeTransition(.switchWindows(draggedWindow, framedWindow)) return } } // Ignore if there is no window at that point guard let secondWindow = WindowsInformation.alternateWindowForScreenAtPoint(pointerLocation, withWindows: windows, butNot: draggedWindow) else { return } executeTransition(.switchWindows(draggedWindow, secondWindow)) } } // MARK: ApplicationObservationDelegate extension WindowManager: ApplicationObservationDelegate { func application(_ application: AnyApplication, didAddWindow window: Window) { add(window: window) } func application(_ application: AnyApplication, didRemoveWindow window: Window) { remove(window: window) } func application(_ application: AnyApplication, didFocusWindow window: Window) { guard let screen = window.screen() else { return } lastFocusDate = Date() if pendingTabDetection.removeValue(forKey: window.id()) != nil { completeTabDetection(for: window, on: screen) } else if windows.isWindowTracked(window) { distributeEventToScreen(screen, change: .focusChanged(window: window)) markScreenForReflow(screen) } else { // Focus notification arrived before the creation notification. // Record this so swapInTab can call completeTabDetection immediately // rather than deferring to a focus event that has already passed. log.debug("Focused untracked window before creation notification - recording early focus: \(window)") earlyFocusedWindows.insert(window.id()) } // doMouseFollowsFocus(focusedWindow: window) } func application(_ application: AnyApplication, didFindPotentiallyNewWindow window: Window) { guard !windows.isWindowTracked(window) else { return } swapInTab(window: window) } func application(_ application: AnyApplication, didMoveWindow window: Window) { guard userConfiguration.mouseSwapsWindows() else { return } guard let screen = window.screen(), activeWindows(on: screen).contains(window) else { return } switch mouseStateKeeper.state { case .dragging: // be aware of last reflow time, again to prevent race condition let reflowEndInterval = Date().timeIntervalSince(lastReflowTime) guard reflowEndInterval > mouseStateKeeper.dragRaceThresholdSeconds else { break } // record window and wait for mouse up mouseStateKeeper.state = .moving(window: window) case let .doneDragging(lmbUpMoment): mouseStateKeeper.state = .pointing // flip state first to prevent race condition // if mouse button recently came up, assume window move is related let dragEndInterval = Date().timeIntervalSince(lmbUpMoment) guard dragEndInterval < mouseStateKeeper.dragRaceThresholdSeconds else { break } mouseStateKeeper.swapDraggedWindowWithDropzone(window) default: break } } func application(_ application: AnyApplication, didResizeWindow window: Window) { guard userConfiguration.mouseResizesWindows() else { return } guard let screen = window.screen(), activeWindows(on: screen).contains(window) else { return } guard let screenManager: ScreenManager> = focusedScreenManager(), let layout = screenManager.currentLayout, layout is PanedLayout else { return } guard let oldFrame = layout.assignedFrame(window, of: windows.windowSet(forActiveWindowsOnScreen: screen), on: screen) else { return } let ratio = oldFrame.impliedMainPaneRatio(windowFrame: window.frame()) switch mouseStateKeeper.state { case .dragging, .resizing: // record window and wait for mouse up mouseStateKeeper.state = .resizing(screen: screen, ratio: ratio) case let .doneDragging(lmbUpMoment): // if mouse button recently came up, assume window resize is related let dragEndInterval = Date().timeIntervalSince(lmbUpMoment) if dragEndInterval < mouseStateKeeper.dragRaceThresholdSeconds { mouseStateKeeper.state = .pointing // flip state first to prevent race condition if let screenManager: ScreenManager> = focusedScreenManager() { screenManager.updateCurrentLayout { layout in if let panedLayout = layout as? PanedLayout { panedLayout.recommendMainPaneRatio(ratio) } } } } default: break } } func applicationDidActivate(_ application: AnyApplication) { NSObject.cancelPreviousPerformRequests( withTarget: self, selector: #selector(applicationActivated(_:)), object: nil ) perform(#selector(applicationActivated(_:)), with: nil, afterDelay: 0.2) } } // MARK: Transition Coordination extension WindowManager { func screen(at index: Int) -> Screen? { return screenManager(at: index)?.screen } func screenManager(at screenIndex: Int) -> ScreenManager>? { guard screenIndex > -1 && screenIndex < screens.screenManagers.count else { return nil } return screens.screenManagers[screenIndex] } func screenManager(for screen: Screen) -> ScreenManager>? { return screens.screenManagers.first { $0.screen?.screenID() == screen.screenID() } } func screenManagerIndex(for screen: Screen) -> Int? { return screens.screenManagers.firstIndex { $0.screen?.screenID() == screen.screenID() } } } // MARK: Window Transition extension WindowManager: WindowTransitionTarget { func executeTransition(_ transition: WindowTransition) { switch transition { case let .switchWindows(window, otherWindow): guard windows.swap(window: window, withWindow: otherWindow) else { return } distributeEventToAllScreens(.windowSwap(window: window, otherWindow: otherWindow)) markAllScreensForReflow() case let .moveWindowToScreen(window, screen): let currentScreen = window.screen() window.moveScaled(to: screen) if currentScreen != nil { distributeEventToScreen(screen, change: .remove(window: window)) markScreenForReflow(screen) } distributeEventToScreen(screen, change: .add(window: window)) window.focus() case let .moveWindowToSpaceAtIndex(window, spaceIndex, sourceSpaceIndex): guard let screen = window.screen(), let spaces = CGSpacesInfo.spacesForAllScreens(includeOnlyUserSpaces: true), spaceIndex < spaces.count else { return } let targetSpace = spaces[spaceIndex] guard let targetScreen = CGSpacesInfo.screenForSpace(space: targetSpace) else { return } distributeEventToScreen(screen, change: .remove(window: window)) eventQueue.append(PendingEvent(screen: targetScreen, event: .add(window: window))) window.move(toSpaceAtIndex: UInt(spaceIndex + 1)) if targetScreen.screenID() != screen.screenID() { // necessary to set frame here as window is expected to be at origin relative to targe screen when moved, can be improved. window.moveScaled(to: targetScreen) } DispatchQueue.main.asyncAfter(deadline: .now() + 1) { if !UserConfiguration.shared.followWindowsThrownBetweenSpaces() { SISystemWideElement.switch(toSpace: UInt(sourceSpaceIndex + 1)) } } case .resetFocus: if let screen = screens.screenManagers.first?.screen { executeTransition(.focusScreen(screen)) } } } func isWindowFloating(_ window: Window) -> Bool { return windows.isWindowFloating(window) } func currentLayout() -> Layout? { return focusedScreenManager()?.currentLayout } func activeWindows(on screen: Screen) -> [Window] { return windows.activeWindows(onScreen: screen).filter { window in return window.shouldBeManaged() && !self.windows.isWindowFloating(window) } } func nextScreenIndexClockwise(from screen: Screen) -> Int { guard let screenManagerIndex = self.screenManagerIndex(for: screen) else { return -1 } return (screenManagerIndex + 1) % (screens.screenManagers.count) } func nextScreenIndexCounterClockwise(from screen: Screen) -> Int { guard let screenManagerIndex = self.screenManagerIndex(for: screen) else { return -1 } return (screenManagerIndex == 0 ? screens.screenManagers.count - 1 : screenManagerIndex - 1) } func lastMainWindowForCurrentSpace() -> Window? { guard let currentFocusedSpace = CGSpacesInfo.currentFocusedSpace(), let lastMainWindow = windows.lastMainWindows[currentFocusedSpace.id] else { return nil } return lastMainWindow } } // MARK: Focus Transition extension WindowManager: FocusTransitionTarget { func windows(onScreen screen: Screen) -> [Window] { return windows.activeWindows(onScreen: screen) } func executeTransition(_ transition: FocusTransition) { switch transition { case let .focusWindow(window): window.focus() case let .focusScreen(screen): screen.focusScreen() } } func lastFocusedWindow(on screen: Screen) -> Window? { return screens.screenManagers.first { $0.screen?.screenID() == screen.screenID() }?.lastFocusedWindow } func nextWindowIDClockwise(on screen: Screen) -> Window.WindowID? { return screenManager(for: screen)?.nextWindowIDClockwise() } func nextWindowIDCounterClockwise(on screen: Screen) -> Window.WindowID? { return screenManager(for: screen)?.nextWindowIDCounterClockwise() } } extension WindowManager: ScreenManagerDelegate { func applyWindowLimit(forScreenManager screenManager: ScreenManager>, minimizingIn range: (Int) -> Range) { guard let screen = screenManager.screen else { return } let windows = screenManager.currentLayout is FloatingLayout ? self.windows(onScreen: screen).filter { $0.shouldBeManaged() } : activeWindows(on: screen) windows[range(windows.count)].forEach { $0.minimize() } } func activeWindowSet(forScreenManager screenManager: ScreenManager>) -> WindowSet { return windows.windowSet(forActiveWindowsOnScreen: screenManager.screen!) } } ================================================ FILE: Amethyst/Managers/WindowTransitionCoordinator.swift ================================================ // // WindowTransitionCoordinator.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/24/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation import Silica enum WindowTransition { typealias Screen = Window.Screen case switchWindows(_ window1: Window, _ window2: Window) case moveWindowToScreen(_ window: Window, screen: Screen) case moveWindowToSpaceAtIndex(_ window: Window, spaceIndex: Int, sourceSpaceIndex: Int) case resetFocus } protocol WindowTransitionTarget: AnyObject { associatedtype Application: ApplicationType typealias Window = Application.Window typealias Screen = Window.Screen func executeTransition(_ transition: WindowTransition) func isWindowFloating(_ window: Window) -> Bool func currentLayout() -> Layout? func screen(at index: Int) -> Screen? func activeWindows(on screen: Screen) -> [Window] func nextScreenIndexClockwise(from screen: Screen) -> Int func nextScreenIndexCounterClockwise(from screen: Screen) -> Int func lastMainWindowForCurrentSpace() -> Window? } class WindowTransitionCoordinator { typealias Window = Target.Window typealias Screen = Window.Screen weak var target: Target? init() {} func swapFocusedWindowToMain() { guard let focusedWindow = Window.currentlyFocused(), target?.isWindowFloating(focusedWindow) == false, let screen = focusedWindow.screen() else { return } guard let windows = target?.activeWindows(on: screen), let focusedIndex = windows.firstIndex(of: focusedWindow) else { return } if windows.count <= 1 { return } if focusedIndex == 0 { guard let lastMainWindow = target?.lastMainWindowForCurrentSpace() else { return } target?.executeTransition(.switchWindows(lastMainWindow, focusedWindow)) return } if focusedIndex != 0 { // Swap focused window with main window if other window is focused target?.executeTransition(.switchWindows(focusedWindow, windows[0])) } } func swapFocusedWindowCounterClockwise() { guard let focusedWindow = Window.currentlyFocused(), target?.isWindowFloating(focusedWindow) == false else { target?.executeTransition(.resetFocus) return } guard let screen = focusedWindow.screen() else { return } guard let windows = target?.activeWindows(on: screen), let focusedWindowIndex = windows.firstIndex(of: focusedWindow) else { return } let windowToSwapWith = windows[(focusedWindowIndex == 0 ? windows.count - 1 : focusedWindowIndex - 1)] target?.executeTransition(.switchWindows(focusedWindow, windowToSwapWith)) } func swapFocusedWindowClockwise() { guard let focusedWindow = Window.currentlyFocused(), target?.isWindowFloating(focusedWindow) == false else { target?.executeTransition(.resetFocus) return } guard let screen = focusedWindow.screen() else { return } guard let windows = target?.activeWindows(on: screen), let focusedWindowIndex = windows.firstIndex(of: focusedWindow) else { return } let windowToSwapWith = windows[(focusedWindowIndex + 1) % windows.count] target?.executeTransition(.switchWindows(focusedWindow, windowToSwapWith)) } func throwToScreenAtIndex(_ screenIndex: Int) { guard let screen = target?.screen(at: screenIndex), let focusedWindow = Window.currentlyFocused() else { return } // If the window is already on the screen do nothing. guard let focusedScreen = focusedWindow.screen(), focusedScreen.screenID() != screen.screenID() else { return } target?.executeTransition(.moveWindowToScreen(focusedWindow, screen: screen)) } func swapFocusedWindowScreenClockwise() { guard let focusedWindow = Window.currentlyFocused(), target?.isWindowFloating(focusedWindow) == false else { target?.executeTransition(.resetFocus) return } guard let screen = focusedWindow.screen() else { return } guard let nextScreenIndex = target?.nextScreenIndexClockwise(from: screen), let nextScreen = target?.screen(at: nextScreenIndex) else { return } target?.executeTransition(.moveWindowToScreen(focusedWindow, screen: nextScreen)) } func swapFocusedWindowScreenCounterClockwise() { guard let focusedWindow = Window.currentlyFocused(), target?.isWindowFloating(focusedWindow) == false else { target?.executeTransition(.resetFocus) return } guard let screen = focusedWindow.screen() else { return } guard let nextScreenIndex = target?.nextScreenIndexCounterClockwise(from: screen), let nextScreen = target?.screen(at: nextScreenIndex) else { return } target?.executeTransition(.moveWindowToScreen(focusedWindow, screen: nextScreen)) } func pushFocusedWindowToSpace(_ space: Int) { guard let currentFocusedSpace = CGSpacesInfo.currentFocusedSpace(), let spaces = CGSpacesInfo.spacesForAllScreens() else { return } guard let index = spaces.firstIndex(of: currentFocusedSpace), index < spaces.count else { return } pushFocusedWindowToSpace(space, sourceSpace: index) } func pushFocusedWindowToSpace(_ space: Int, sourceSpace: Int) { guard let focusedWindow = Window.currentlyFocused(), focusedWindow.screen() != nil else { return } target?.executeTransition(.moveWindowToSpaceAtIndex(focusedWindow, spaceIndex: space, sourceSpaceIndex: sourceSpace)) } func pushFocusedWindowToSpaceLeft() { guard let currentFocusedSpace = CGSpacesInfo.currentFocusedSpace(), let spaces = CGSpacesInfo.spacesForAllScreens() else { return } let filteredSpaces = spaces.filter { $0.type == CGSSpaceTypeUser } guard let index = filteredSpaces.firstIndex(of: currentFocusedSpace), index > 0 else { return } pushFocusedWindowToSpace(index - 1, sourceSpace: index) } func pushFocusedWindowToSpaceRight() { guard let currentFocusedSpace = CGSpacesInfo.currentFocusedSpace(), let spaces = CGSpacesInfo.spacesForAllScreens() else { return } let filteredSpaces = spaces.filter { $0.type == CGSSpaceTypeUser } guard let index = filteredSpaces.firstIndex(of: currentFocusedSpace), index + 1 < spaces.count else { return } pushFocusedWindowToSpace(index + 1, sourceSpace: index) } } ================================================ FILE: Amethyst/Managers/Windows.swift ================================================ // // Windows.swift // Amethyst // // Created by Ian Ynda-Hummel on 9/15/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica extension WindowManager { class Windows { private(set) var windows: [Window] = [] private(set) var lastMainWindows: [CGSSpaceID: Window?] = [:] private var activeIDCache: Set = Set() private var deactivatedPIDs: Set = Set() private var floatingMap: [Window.WindowID: Bool] = [:] // MARK: Window Filters func window(withID id: Window.WindowID) -> Window? { return windows.first { $0.id() == id } } func windows(forApplicationWithPID applicationPID: pid_t) -> [Window] { return windows.filter { $0.pid() == applicationPID } } func windows(onScreen screen: Screen) -> [Window] { return windows.filter { $0.screen() == screen } } func activeWindows(onScreen screen: Screen) -> [Window] { guard let screenID = screen.screenID() else { return [] } guard let currentSpace = CGSpacesInfo.currentSpaceForScreen(screen) else { log.warning("Could not find a space for screen: \(screenID)") return [] } let screenWindows = windows.filter { window in let space = CGWindowsInfo.windowSpace(window) guard let windowScreen = window.screen(), currentSpace.id == space else { return false } let isActive = self.isWindowActive(window) let isHidden = self.isWindowHidden(window) let isFloating = self.isWindowFloating(window) return windowScreen.screenID() == screen.screenID() && isActive && !isHidden && !isFloating } return screenWindows } func activeWindowOnCurrentScreen(atIndex: Int) -> Window? { guard let focusedWindow = Window.currentlyFocused(), let currentScreen = focusedWindow.screen() else { return nil } let activeWindows = activeWindows(onScreen: currentScreen) return activeWindows.indices.contains(atIndex) ? activeWindows[atIndex] : nil } // MARK: Adding and Removing func add(window: Window, atFront shouldInsertAtFront: Bool) { if shouldInsertAtFront { if let currentFocusedSpace = CGSpacesInfo.currentFocusedSpace(), let firstActiveWindow = activeWindowOnCurrentScreen(atIndex: 0) { lastMainWindows[currentFocusedSpace.id] = firstActiveWindow } windows.insert(window, at: 0) } else { windows.append(window) } } func add(window: Window, afterWindow otherWindow: Window) -> Bool { guard let otherWindowIndex = windows.firstIndex(of: otherWindow) else { return false } windows.insert(window, at: otherWindowIndex) return true } func remove(window: Window) { for (_, lastMainWindow) in lastMainWindows where lastMainWindow?.id() == window.id() { if let currentFocusedSpace = CGSpacesInfo.currentFocusedSpace() { let secondWindow = activeWindowOnCurrentScreen(atIndex: 1) lastMainWindows[currentFocusedSpace.id] = secondWindow } } guard let windowIndex = windows.firstIndex(where: { $0.id() == window.id() }) else { return } windows.remove(at: windowIndex) } @discardableResult func replace(window: Window, withWindow otherWindow: Window) -> Bool { if let currentFocusedSpace = CGSpacesInfo.currentFocusedSpace(), let firstActiveWindow = activeWindowOnCurrentScreen(atIndex: 0) { if firstActiveWindow == window || firstActiveWindow == otherWindow { lastMainWindows[currentFocusedSpace.id] = firstActiveWindow } } guard let otherWindowIndex = windows.firstIndex(of: otherWindow) else { windows.append(otherWindow) return false } let windowIndex = windows.firstIndex(of: window) windows[otherWindowIndex] = window if let windowIndex { windows.remove(at: windowIndex) } return true } @discardableResult func swap(window: Window, withWindow otherWindow: Window) -> Bool { if let currentFocusedSpace = CGSpacesInfo.currentFocusedSpace(), let firstActiveWindow = activeWindowOnCurrentScreen(atIndex: 0) { if firstActiveWindow.id() == window.id() || firstActiveWindow.id() == otherWindow.id() { lastMainWindows[currentFocusedSpace.id] = firstActiveWindow } } if windows.firstIndex(of: window) == nil { windows.append(window) } guard let windowIndex = windows.firstIndex(of: window), let otherWindowIndex = windows.firstIndex(of: otherWindow) else { return false } guard windowIndex != otherWindowIndex else { return false } windows[windowIndex] = otherWindow windows[otherWindowIndex] = window return true } // MARK: Window States func isWindowTracked(_ window: Window) -> Bool { return windows.contains(where: { $0.id() == window.id() }) } func isWindowActive(_ window: Window) -> Bool { return window.isActive() && activeIDCache.contains(window.cgID()) } func isWindowHidden(_ window: Window) -> Bool { return deactivatedPIDs.contains(window.pid()) } func isWindowFloating(_ window: Window) -> Bool { return floatingMap[window.id()] ?? false } func setFloating(_ floating: Bool, forWindow window: Window) { floatingMap[window.id()] = floating } func activateApplication(withPID pid: pid_t) { deactivatedPIDs.remove(pid) } func deactivateApplication(withPID pid: pid_t) { deactivatedPIDs.insert(pid) } func regenerateActiveIDCache() { let windowDescriptions = CGWindowsInfo(options: .optionOnScreenOnly, windowID: CGWindowID(0)) activeIDCache = windowDescriptions?.activeIDs() ?? Set() } // MARK: Window Sets func windowSet(forWindowsOnScreen screen: Screen) -> WindowSet { return windowSet(forWindows: windows(onScreen: screen)) } func windowSet(forActiveWindowsOnScreen screen: Screen) -> WindowSet { return windowSet(forWindows: activeWindows(onScreen: screen)) } func windowSet(forWindows windows: [Window]) -> WindowSet { let layoutWindows: [LayoutWindow] = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: $0.isFocused()) } return WindowSet( windows: layoutWindows, isWindowWithIDActive: { [weak self] id -> Bool in guard let window = self?.window(withID: id) else { return false } return self?.isWindowActive(window) ?? false }, isWindowWithIDFloating: { [weak self] windowID -> Bool in guard let window = self?.window(withID: windowID) else { return false } return self?.isWindowFloating(window) ?? false }, windowForID: { [weak self] windowID -> Window? in return self?.window(withID: windowID) } ) } } } ================================================ FILE: Amethyst/Model/Application.swift ================================================ // // Application.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/12/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import AppKit import Foundation import Silica /// Generic protocol for objects acting as applications in the system. protocol ApplicationType: Equatable { /// The type of windows that are used by the application. associatedtype Window: WindowType /** Initialize an application based on its corresponding `NSRunningApplication` if it exists. - Parameters: - runningApplication: The running application to find application for. */ init(runningApplication: NSRunningApplication) /// The optional title of the application func title() -> String? /** The windows owned by the application. - Note: This value is cached. Call `dropWindowsCache()` if you believe this may be out of date. */ func windows() -> [Window] /// The process ID of the application. func pid() -> pid_t /** Determines whether a window should float by default. - Parameters: - window: The window to test. - Note: We can receive an unreliable result. It is up to the caller to determine whether or not that result is good enough. */ func defaultFloatForWindow(_ window: Window) -> Reliable /// Clears the internal cache of application windows. func dropWindowsCache() /** Observe an AX notification on the application itself with a given handler. To remove the observation you must call `unobserve(notification:)`. - Parameters: - notification: The notification name to be observing for. - handler: The callback when the notification is triggered. - Returns: `true` if observing the notification succeeded, and `false` otherwise. */ func observe(notification: String, handler: @escaping SIAXNotificationHandler) -> AXError /** Observe an AX notification on a window of the application with a given handler. To remove the observation you must call `unobserve(notification:window:)`. - Parameters: - notification: The notification name to be observing for. - window: The window being watched for events. - handler: The callback when the notification is triggered. - Returns: `true` if observing the notification succeeded, and `false` otherwise. */ func observe(notification: String, window: Window, handler: @escaping SIAXNotificationHandler) -> AXError /** Removes an observation for a notification on the application itself. - Parameters: - notification: The notification name to stop observing for. */ func unobserve(notification: String) /** Removes an observation for a notification on the application itself. - Parameters: - notification: The notification name to stop observing for. - window: The window to stop watching for events. */ func unobserve(notification: String, window: Window) } /** Type-erased concerete application for managing applications. This is necessitated by `ApplicationType` having an associated type which prevents it from being used directly as a concrete type. */ class AnyApplication: ApplicationType { /// The window being used is the window being used by the contained application type. typealias Window = Application.Window /// The application being contained. private let internalApplication: Application /// Comparison for `Equatable` conformance. static func == (lhs: AnyApplication, rhs: AnyApplication) -> Bool { return lhs.internalApplication == rhs.internalApplication } /** Initializes an application based on another application. - Parameters: - application: The application to be contained. */ required init(_ application: Application) { self.internalApplication = application } /// It is an error to call this initializer. required init(runningApplication: NSRunningApplication) { fatalError() } func title() -> String? { return internalApplication.title() } func windows() -> [Window] { return internalApplication.windows() } func pid() -> pid_t { return internalApplication.pid() } func defaultFloatForWindow(_ window: Window) -> Reliable { return internalApplication.defaultFloatForWindow(window) } func dropWindowsCache() { internalApplication.dropWindowsCache() } func observe(notification: String, handler: @escaping SIAXNotificationHandler) -> AXError { return internalApplication.observe(notification: notification, handler: handler) } func observe(notification: String, window: Window, handler: @escaping SIAXNotificationHandler) -> AXError { return internalApplication.observe(notification: notification, window: window, handler: handler) } func unobserve(notification: String) { internalApplication.unobserve(notification: notification) } func unobserve(notification: String, window: Window) { internalApplication.unobserve(notification: notification, window: window) } } /// Conformance of `SIApplication` as an Amethyst application. extension SIApplication: ApplicationType { /// `SIApplication` uses `AXWindow` as its window type. typealias Window = AXWindow convenience init?(pid: pid_t) { guard let runningApplication = NSRunningApplication(processIdentifier: pid) else { return nil } self.init(runningApplication: runningApplication) } func windows() -> [Window] { let axWindows: [SIWindow] = self.windows() return axWindows.map { Window(axElement: $0.axElementRef) } } func pid() -> pid_t { return processIdentifier() } func observe(notification: String, handler: @escaping SIAXNotificationHandler) -> AXError { return observeNotification(notification as CFString, with: self, handler: handler) } func observe(notification: String, window: Window, handler: @escaping SIAXNotificationHandler) -> AXError { return observeNotification(notification as CFString, with: window, handler: handler) } func unobserve(notification: String) { unobserveNotification(notification as CFString, with: self) } func unobserve(notification: String, window: Window) { unobserveNotification(notification as CFString, with: window) } func defaultFloatForWindow(_ window: Window) -> Reliable { if window.shouldFloat() { return .reliable(.floating) } guard let runningApplication = NSRunningApplication(processIdentifier: pid()) else { return .reliable(.floating) } return UserConfiguration.shared.runningApplication(runningApplication, byDefaultFloatsForTitle: window.title()) } private func observe(notification: String, with accessibilityElement: SIAccessibilityElement, handler: @escaping SIAXNotificationHandler) -> AXError { return observeNotification(notification as CFString, with: accessibilityElement, handler: handler) } } ================================================ FILE: Amethyst/Model/ApplicationEventHandler.swift ================================================ // // ApplicationEventHandler.swift // Amethyst // // Created by Ian Ynda-Hummel on 2/28/23. // Copyright © 2023 Ian Ynda-Hummel. All rights reserved. // import Carbon import Foundation // swiftlint:disable identifier_name @_silgen_name("GetProcessPID") @discardableResult func GetProcessPID(_ psn: inout ProcessSerialNumber, _ pid: inout pid_t) -> OSStatus // swiftlint:enable identifier_name protocol ApplicationEventHandlerDelegate: AnyObject { func add(applicationWithPID: pid_t) func remove(applicationWithPID: pid_t) } func applicationEventHandlerUPP(_ call: EventHandlerCallRef?, _ event: EventRef?, _ data: UnsafeMutableRawPointer?) -> OSStatus { guard let data = data, let event = event else { return OSStatus(eventNotHandledErr) } let handler = Unmanaged.fromOpaque(data).takeUnretainedValue() return handler.handleEvent(event) } class ApplicationEventHandler { private enum EventType { case applicationLaunched case applicationTerminated } private enum EventError: Error { case failedToGetPSN case failedToGetPID case processNotFound } private struct Event { let ref: EventRef let eventType: EventType func pid() throws -> pid_t { var psn = ProcessSerialNumber() var error = GetEventParameter(ref, EventParamName(kEventParamProcessID), EventParamName(typeProcessSerialNumber), nil, MemoryLayout.size, nil, &psn) guard error == noErr else { log.error(error) throw EventError.failedToGetPSN } var pid = pid_t() error = GetProcessPID(&psn, &pid) guard error == noErr else { switch error { case OSStatus(procNotFound): throw EventError.processNotFound default: throw EventError.failedToGetPID } } return pid } } weak var delegate: ApplicationEventHandlerDelegate? init(delegate: ApplicationEventHandlerDelegate) { self.delegate = delegate } func handleEvent(_ event: EventRef) -> OSStatus { switch GetEventKind(event) { case UInt32(kEventAppLaunched): return processEvent(Event(ref: event, eventType: .applicationLaunched)) case UInt32(kEventAppTerminated): return processEvent(Event(ref: event, eventType: .applicationTerminated)) default: return OSStatus(eventNotHandledErr) } } private func processEvent(_ event: Event) -> OSStatus { do { let pid = try event.pid() switch event.eventType { case .applicationLaunched: delegate?.add(applicationWithPID: pid) case .applicationTerminated: delegate?.remove(applicationWithPID: pid) } } catch { log.error(error) return OSStatus(eventNotHandledErr) } return noErr } } ================================================ FILE: Amethyst/Model/ApplicationObservation.swift ================================================ // // ApplicationObservation.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/21/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import AppKit import Cocoa import Foundation import RxSwift /// Delegate for handling application observer events. protocol ApplicationObservationDelegate: AnyObject { associatedtype Application: ApplicationType typealias Window = Application.Window /** Called when the application has added a window to being active. - Parameters: - application: The application the event occurred in. - window: The window that was added. - Note: `window` is not necessarily newly created. */ func application(_ application: AnyApplication, didAddWindow window: Window) /** Called when the application has removed a window from being active. - Parameters: - application: The application the event occurred in. - window: The window that was removed. */ func application(_ application: AnyApplication, didRemoveWindow window: Window) /** Called when the application has focused a window. - Parameters: - application: The application the event occurred in. - window: The window that was focused. */ func application(_ application: AnyApplication, didFocusWindow window: Window) /** Called when the application has encountered a window that is potentially new. - Parameters: - application: The application the event occurred in. - window: The window that was encountered. - Note: This is the event that is called when native tab switching happens. */ func application(_ application: AnyApplication, didFindPotentiallyNewWindow window: Window) /** Called when the application has moved a window. - Parameters: - application: The application the event occurred in. - window: The window that was moved. */ func application(_ application: AnyApplication, didMoveWindow window: Window) /** Called when the application has resized a window. - Parameters: - application: The application the event occurred in. - window: The window that was resized. */ func application(_ application: AnyApplication, didResizeWindow window: Window) /** Called when the application is activated. - Parameters: - application: The application that was activated. */ func applicationDidActivate(_ application: AnyApplication) } /** This struct sets up accessibility API event subscriptions for a given Application. Handling references to the window manager and mouse state. The observers themselves react to mouse / accessibility state by either changing window positions or updating the mouse state based on new information. */ struct ApplicationObservation { typealias Application = Delegate.Application typealias Window = Application.Window /// Errors when attempting to add observers to applications enum Error: Swift.Error { /// General failure case failed /// Failure in the accessibility observation case observationFailed(error: Int32) } /// Notifications that are observed private enum Notification { /// A window is created case created /// A window is deminiaturized case windowDeminiaturized /// A window is miniaturized case windowMiniaturized /// The application has changed its focused window case focusedWindowChanged /// The application is activated case applicationActivated /// A window is moved case windowMoved /// A window is resized case windowResized /// The application changed its primary window case mainWindowChanged /// The window has likely been destroyed case elementDestroyed(window: Window) /// The actual notification name var string: String { switch self { case .created: return kAXCreatedNotification case .windowDeminiaturized: return kAXWindowDeminiaturizedNotification case .windowMiniaturized: return kAXWindowMiniaturizedNotification case .focusedWindowChanged: return kAXFocusedWindowChangedNotification case .applicationActivated: return kAXApplicationActivatedNotification case .windowMoved: return kAXWindowMovedNotification case .windowResized: return kAXWindowResizedNotification case .mainWindowChanged: return kAXMainWindowChangedNotification case .elementDestroyed: return kAXUIElementDestroyedNotification } } /// Notifications relevant to the entire application static var applicationNotifications: [Notification] { return [ .created, .windowDeminiaturized, .windowMiniaturized, .focusedWindowChanged, .applicationActivated, .windowMoved, .windowResized, .mainWindowChanged ] } /// Notifications relevant to a particular window of an application static func windowNotificationsForWindow(_ window: Window) -> [Notification] { return [ .elementDestroyed(window: window) ] } } /// The application being observed let application: AnyApplication /// The delegate for handling events as they come in private weak var delegate: Delegate? /** - Parameters: - application: The application to be observed. - delegate: The delegate to handle events. */ init(application: AnyApplication, delegate: Delegate?) { self.application = application self.delegate = delegate } /** - Returns: An observable that attemps to subscribe to events on the application. The observable completes when subscriptions have been put in place, and errors otherwise. */ func addObservers() -> Observable { return _addObservers(notifications: Notification.applicationNotifications).retry { errorTrigger in errorTrigger.enumerated().flatMap { count, error -> Observable in guard count < 6 else { return .error(error) } return .timer(.milliseconds((count ^ 2 * 100)), scheduler: MainScheduler.instance) } } } /** - Returns: An observable that attempts to subscribe to events specifically relevant to a window in an application. The observable completes when subscriptions have been put in place, and errors otherwise. */ func addObserversForWindow(_ window: Window) -> Observable { let notifications = Notification.windowNotificationsForWindow(window) return _addObservers(notifications: notifications).retry { errorTrigger in errorTrigger.enumerated().flatMap { count, error -> Observable in guard count < 6 else { return .error(error) } return .timer(.milliseconds((count ^ 2 * 100)), scheduler: MainScheduler.instance) } } } /** - Returns: An observable that unsubscribes from events that may be tracked for a specific window. - Parameters: - window: the window on which notifications may be observed. - Note: This is a no op in the case where no notifications were being observed. */ func removeObserversForWindow(_ window: Window) { let notifications = Notification.windowNotificationsForWindow(window) removeObservers(notifications: notifications, for: window) } private func _addObservers(notifications: [Notification]) -> Observable { return Observable.from(notifications) .scan([]) { observed, notification -> [Notification] in let notifications = observed + [notification] do { try self.addObserver(for: notification) } catch { let applicationTitle = self.application.title() ?? "" log.error("Failed to add observer \(notification) on application \(applicationTitle) (\(self.application.pid())): \(error)") self.removeObservers(notifications: notifications) throw error } return notifications } .map { _ in } } /** Observes a specific notification. - Parameters: - notification: The notification to observe. - Throws: An error when failing to add observer. */ private func addObserver(for notification: Notification) throws { let success: AXError switch notification { case .elementDestroyed(let window): success = application.observe(notification: notification.string, window: window) { _ in DispatchQueue.main.async { self.handle(notification: notification, window: window) } } default: success = application.observe(notification: notification.string) { element in guard let window = Window(element: element) else { return } DispatchQueue.main.async { self.handle(notification: notification, window: window) } } } switch success { case .success: return case .notificationAlreadyRegistered: return default: throw Error.observationFailed(error: success.rawValue) } } /** Removes notifications from being observed. - Parameters: - notifications: The notifications to stop observing. */ private func removeObservers(notifications: [Notification]) { notifications.forEach { application.unobserve(notification: $0.string) } } /** Removes notifications from being observed for a window. - Parameters: - notifications: the notifications to stop observing. - window: the window for which notifications were being observed. */ private func removeObservers(notifications: [Notification], for window: Window) { notifications.forEach { application.unobserve(notification: $0.string, window: window) } } private func handle(notification: Notification, window: Window) { log.debug(""" Received notification for window: \(window) notification: \(notification) \(window.title() ?? "no title") (\(window.id())) """) switch notification { case .created: // Disabling window creations because they end up being more reliably tracked with main window changed // delegate?.application(application, didFindPotentiallyNewWindow: window) break case .windowDeminiaturized: delegate?.application(application, didAddWindow: window) case .windowMiniaturized: delegate?.application(application, didRemoveWindow: window) case .focusedWindowChanged: guard let focusedWindow = Window.currentlyFocused() else { return } delegate?.application(application, didFocusWindow: focusedWindow) case .applicationActivated: delegate?.applicationDidActivate(application) case .windowMoved: delegate?.application(application, didMoveWindow: window) case .windowResized: delegate?.application(application, didResizeWindow: window) case .mainWindowChanged: delegate?.application(application, didFindPotentiallyNewWindow: window) case .elementDestroyed: delegate?.application(application, didRemoveWindow: window) } } } ================================================ FILE: Amethyst/Model/CGInfo.swift ================================================ // // CGInfo.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/29/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica import SwiftyJSON /// Windows info as taken from the underlying system. struct CGWindowsInfo { /// An array of dictionaries of window information let descriptions: [[String: AnyObject]] /** - Parameters: - options: Any options for getting info. - windowID: ID of window to find windows relative to. 0 gets all windows. */ init?(options: CGWindowListOption, windowID: CGWindowID) { guard let cfWindowDescriptions = CGWindowListCopyWindowInfo(options, windowID) else { return nil } guard let windowDescriptions = cfWindowDescriptions as? [[String: AnyObject]] else { return nil } self.descriptions = windowDescriptions } /** - Returns: The set of windows that are currently active. */ func activeIDs() -> Set { var ids: Set = Set() for windowDescription in descriptions { guard let windowID = windowDescription[kCGWindowNumber as String] as? NSNumber else { continue } ids.insert(CGWindowID(windowID.uint64Value)) } return ids } static func windowIDsArray(_ window: Window) -> NSArray { return [NSNumber(value: window.cgID() as UInt32)] as NSArray } static func windowSpace(_ window: Window) -> Int? { let windowIDsArray = CGWindowsInfo.windowIDsArray(window) guard let cfSpaces = CGSCopySpacesForWindows(CGSMainConnectionID(), kCGSAllSpacesMask, windowIDsArray)?.takeRetainedValue() else { return nil } guard let spaces = cfSpaces as NSArray as? [NSNumber] else { return nil } guard !spaces.isEmpty else { return nil } return spaces.first?.intValue } } struct CGScreensInfo { typealias Screen = Window.Screen let descriptions: [JSON] init?() { guard let descriptions = Screen.screenDescriptions() else { return nil } self.descriptions = descriptions } func space(at index: Int) -> Space { return CGSpacesInfo.space(fromScreenDescription: descriptions[index]) } } struct CGSpacesInfo { typealias Screen = Window.Screen static func spacesForAllScreens(includeOnlyUserSpaces: Bool = false) -> [Space]? { guard let screenDescriptions = Screen.screenDescriptions() else { return nil } guard !screenDescriptions.isEmpty else { return nil } let spaces = screenDescriptions.map { screenDescription -> [Space] in return allSpaces(fromScreenDescription: screenDescription) ?? [] }.reduce([], {acc, spaces in acc + spaces}) if includeOnlyUserSpaces { return spaces.filter { $0.type == CGSSpaceTypeUser } } return spaces } static func spacesForScreen(_ screen: Screen, includeOnlyUserSpaces: Bool = false) -> [Space]? { guard let screenDescriptions = Screen.screenDescriptions() else { return nil } guard !screenDescriptions.isEmpty else { return nil } let screenID = screen.screenID() let spaces: [Space]? if Screen.screensHaveSeparateSpaces { spaces = screenDescriptions .first { $0["Display Identifier"].string == screenID } .flatMap { screenDescription -> [Space]? in return allSpaces(fromScreenDescription: screenDescription) } } else { spaces = allSpaces(fromScreenDescription: screenDescriptions[0]) } if includeOnlyUserSpaces { return spaces?.filter { $0.type == CGSSpaceTypeUser } } return spaces } static func spacesForFocusedScreen() -> [Space]? { guard let focusedWindow = Window.currentlyFocused(), let screen = focusedWindow.screen() else { return nil } return spacesForScreen(screen) } static func currentSpaceForScreen(_ screen: Screen) -> Space? { guard let screenDescriptions = Screen.screenDescriptions(), let screenID = screen.screenID() else { return nil } guard screenDescriptions.count > 0 else { return nil } if Screen.screensHaveSeparateSpaces { for screenDescription in screenDescriptions { guard screenDescription["Display Identifier"].string == screenID else { continue } return space(fromScreenDescription: screenDescription) } } else { return space(fromScreenDescription: screenDescriptions[0]) } return nil } static func currentFocusedSpace() -> Space? { guard let focusedWindow = Window.currentlyFocused(), let screen = focusedWindow.screen() else { return nil } return currentSpaceForScreen(screen) } static func screenForSpace(space: Space) -> Screen? { return Screen.availableScreens.first { spacesForScreen($0)?.contains { $0.id == space.id } ?? false } } static func space(fromScreenDescription screenDictionary: JSON) -> Space { return space(fromSpaceDescription: screenDictionary["Current Space"]) } static func space(fromSpaceDescription spaceDictionary: JSON) -> Space { let id: CGSSpaceID = spaceDictionary["ManagedSpaceID"].intValue let type = CGSSpaceType(rawValue: spaceDictionary["type"].uInt32Value) let uuid = spaceDictionary["uuid"].stringValue return Space(id: id, type: type, uuid: uuid) } static func allSpaces(fromScreenDescription screenDictionary: JSON) -> [Space]? { return screenDictionary["Spaces"].array?.map { space(fromSpaceDescription: $0) } } } ================================================ FILE: Amethyst/Model/Change.swift ================================================ // // Change.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/29/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation enum Change { case add(window: Window) case remove(window: Window) case focusChanged(window: Window) case windowSwap(window: Window, otherWindow: Window) case applicationActivate case applicationDeactivate case spaceChange case layoutChange case tabChange(window: Window, previousWindow: Window) case none case unknown } ================================================ FILE: Amethyst/Model/MouseState.swift ================================================ // // MouseState.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/21/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica /** These are the possible actions that the mouse might be taking (that we care about). We use this enum to convey some information about the window that the mouse might be interacting with. */ enum MouseState { typealias Screen = Window.Screen case pointing case clicking case dragging case moving(window: Window) case resizing(screen: Screen, ratio: CGFloat) case doneDragging(atTime: Date) } /// MouseStateKeeper will need a few things to do its job effectively protocol MouseStateKeeperDelegate: AnyObject { associatedtype Window: WindowType func recommendMainPaneRatio(_ ratio: CGFloat) func swapDraggedWindowWithDropzone(_ draggedWindow: Window) } /** Maintains state information about the mouse for the purposes of mouse-based window operations. MouseStateKeeper exists because we need a single shared mouse state between all applications being observed. This class captures the state and coordinates any Amethyst reflow actions that are required in response to mouse events. Note that some actions may be initiated here and some actions may be completed here; we don't know whether the mouse event stream or the accessibility event stream will fire first. This class by itself can only understand clicking, dragging, and "pointing" (no mouse buttons down). The SIApplication observers are able to augment that understanding of state by "upgrading" a drag action to a "window move" or a "window resize" event since those observers will have proper context. */ class MouseStateKeeper { let dragRaceThresholdSeconds = 0.15 // prevent race conditions during drag ops var state: MouseState private(set) weak var delegate: Delegate? private(set) var lastClick: Date? private var monitor: Any? init(delegate: Delegate) { self.delegate = delegate state = .pointing let mouseEventsToWatch: NSEvent.EventTypeMask = [.leftMouseDown, .leftMouseUp, .leftMouseDragged] monitor = NSEvent.addGlobalMonitorForEvents(matching: mouseEventsToWatch, handler: self.handleMouseEvent) } deinit { guard let oldMonitor = monitor else { return } NSEvent.removeMonitor(oldMonitor) } // Update our understanding of the current state unless an observer has already // done it for us. mouseUp events take precedence over anything an observer had // found -- you can't be dragging or resizing with a mouse button up, even if // you're using the "3 finger drag" accessibility option, where no physical button // is being pressed. func handleMouseEvent(anEvent: NSEvent) { switch anEvent.type { case .leftMouseDown: self.state = .clicking case .leftMouseDragged: switch self.state { case .moving, .resizing: break // ignore - we have what we need case .pointing, .clicking, .dragging, .doneDragging: self.state = .dragging } case .leftMouseUp: switch self.state { case .dragging: // assume window move event will come shortly after self.state = .doneDragging(atTime: Date()) case let .moving(draggedWindow): self.state = .pointing // flip state first to prevent race condition self.swapDraggedWindowWithDropzone(draggedWindow) case let .resizing(_, ratio): self.state = .pointing self.resizeFrameToDraggedWindowBorder(ratio) case .doneDragging: self.state = .doneDragging(atTime: Date()) // reset the clock I guess case .clicking: lastClick = Date() self.state = .pointing case .pointing: self.state = .pointing } default: () } } // React to a reflow event. Typically this means that any window we were dragging // is no longer valid and should be de-correlated from the mouse func handleReflowEvent() { switch self.state { case .doneDragging: self.state = .pointing // remove associated timestamp case .moving: self.state = .dragging // remove associated window default: () } } // Execute an action that was initiated by the observer and completed by the state keeper func resizeFrameToDraggedWindowBorder(_ ratio: CGFloat) { delegate?.recommendMainPaneRatio(ratio) } // Execute an action that was initiated by the observer and completed by the state keeper func swapDraggedWindowWithDropzone(_ draggedWindow: Delegate.Window) { delegate?.swapDraggedWindowWithDropzone(draggedWindow) } } ================================================ FILE: Amethyst/Model/Reliability.swift ================================================ // // Reliability.swift // Amethyst // // Created by Ian Ynda-Hummel on 9/8/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation /// `Relatable` wraps a value with the level of confidence that the value is correct. enum Reliable { /// `reliable` means that the value is probably correct. case reliable(T) /// `unreliable` means that the value may be correct, but the returning function is not confident that the value will remain stable. case unreliable(T) } extension Reliable: Equatable where T: Equatable {} ================================================ FILE: Amethyst/Model/Screen.swift ================================================ // // Screen.swift // Amethyst // // Created by Ian Ynda-Hummel on 9/14/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation import Silica import SwiftyJSON /// Generic protocol for objects acting as screens in the system. protocol ScreenType: Equatable { /// The list of all the screens available to the system. This is assumed to be meaningfuly ordered such that the first screen is the primary screen. static var availableScreens: [Self] { get } /// If `true` this means that each screen has its own set of spaces. If `false` there is only one set of spaces shared by all screens. static var screensHaveSeparateSpaces: Bool { get } /** Descriptions of all screens taken from the underlying graphics system. These are used to correlate information from multiple sources. */ static func screenDescriptions() -> [JSON]? /** The frame adjusted for app modifiers; e.g., window margin - Parameters: - disableWindowMargins: If `true`, then window margins won't be applied */ func adjustedFrame(disableWindowMargins: Bool) -> CGRect /// The frame adjusted to contain both the dock and the status menu. func frameIncludingDockAndMenu() -> CGRect /// The frame adjusted such that the dock and menu are not included. func frameWithoutDockOrMenu() -> CGRect /// The frame without adjustment. func frame() -> CGRect /// The opaque idenfitifer for the screen in the underlying graphics system. func screenID() -> String? /// Raises the window to the foreground. func focusScreen() } extension ScreenType { /** The total height of all screens taking relative layout into account. Depending on the arrangement of multiple screens, it is possible to get a height that is larger than any of the individual screens. This function looks at each display frame's y-coordinates to calculate that height. */ static func globalHeight() -> CGFloat { let maxY = availableScreens.map { $0.frameIncludingDockAndMenu().maxY }.max() ?? 0 let minY = availableScreens.map { $0.frameIncludingDockAndMenu().minY }.min() ?? 0 return maxY - minY } /// The frame adjusted for app modifiers; e.g., window margins. func adjustedFrame() -> CGRect { return adjustedFrame(disableWindowMargins: false) } } struct AMScreen: ScreenType { static var availableScreens: [AMScreen] { return NSScreen.screens.map { AMScreen(screen: $0) } } static var screensHaveSeparateSpaces: Bool { return NSScreen.screensHaveSeparateSpaces } let screen: NSScreen func adjustedFrame(disableWindowMargins: Bool) -> CGRect { var frame = UserConfiguration.shared.ignoreMenuBar() ? frameIncludingDockAndMenu() : frameWithoutDockOrMenu() if UserConfiguration.shared.windowMargins() && !disableWindowMargins { /* Inset for producing half of the full padding around screen as collapse only adds half of it to all windows */ let padding = floor(UserConfiguration.shared.windowMarginSize() / 2) frame.origin.x += padding frame.origin.y += padding frame.size.width -= 2 * padding frame.size.height -= 2 * padding } let windowMinimumWidth = UserConfiguration.shared.windowMinimumWidth() let windowMinimumHeight = UserConfiguration.shared.windowMinimumHeight() if windowMinimumWidth > frame.size.width { frame.origin.x -= (windowMinimumWidth - frame.size.width) / 2 frame.size.width = windowMinimumWidth } if windowMinimumHeight > frame.size.height { frame.origin.y -= (windowMinimumHeight - frame.size.height) / 2 frame.size.height = windowMinimumHeight } let isDisablePaddingOnBuiltinDisplay: Bool = UserConfiguration.shared.disablePaddingOnBuiltinDisplay() let isScreenBuiltin: boolean_t = CGDisplayIsBuiltin(screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID) if isDisablePaddingOnBuiltinDisplay && isScreenBuiltin == 1 {return frame} let paddingTop = UserConfiguration.shared.screenPaddingTop() let paddingBottom = UserConfiguration.shared.screenPaddingBottom() let paddingLeft = UserConfiguration.shared.screenPaddingLeft() let paddingRight = UserConfiguration.shared.screenPaddingRight() frame.origin.y += paddingTop frame.origin.x += paddingLeft // subtract the right padding, and also any amount that we pushed the frame to the left with the left padding frame.size.width -= (paddingRight + paddingLeft) // subtract the bottom padding, and also any amount that we pushed the frame down with the top padding frame.size.height -= (paddingBottom + paddingTop) return frame } func frameIncludingDockAndMenu() -> CGRect { return screen.frameIncludingDockAndMenu() } func frameWithoutDockOrMenu() -> CGRect { return screen.frameWithoutDockOrMenu() } func frame() -> CGRect { return screen.frame } func screenID() -> String? { guard let managedDisplay = CGSCopyBestManagedDisplayForRect(CGSMainConnectionID(), frameIncludingDockAndMenu()) else { return nil } return String(managedDisplay.takeRetainedValue()) } func focusScreen() { let screenFrame = frameIncludingDockAndMenu() let mouseCursorPoint = NSPoint(x: screenFrame.midX, y: screenFrame.midY) let mouseMoveEvent = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: mouseCursorPoint, mouseButton: .left) mouseMoveEvent?.flags = CGEventFlags(rawValue: 0) mouseMoveEvent?.post(tap: .cghidEventTap) } static func screenDescriptions() -> [JSON]? { guard let cfScreenDescriptions = CGSCopyManagedDisplaySpaces(CGSMainConnectionID())?.takeRetainedValue() else { return nil } guard let screenDescriptions = cfScreenDescriptions as NSArray as? [[String: AnyObject]] else { return nil } return screenDescriptions.map { JSON($0) } } } ================================================ FILE: Amethyst/Model/Space.swift ================================================ // // Space.swift // Amethyst // // Created by Ian Ynda-Hummel on 9/9/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica struct Space: Equatable { let id: CGSSpaceID let type: CGSSpaceType let uuid: String } ================================================ FILE: Amethyst/Model/Window.swift ================================================ // // Window.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/10/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import Foundation import Silica // swiftlint:disable identifier_name @_silgen_name("GetProcessForPID") @discardableResult func GetProcessForPID(_ pid: pid_t, _ psn: inout ProcessSerialNumber) -> OSStatus @_silgen_name("_SLPSSetFrontProcessWithOptions") @discardableResult func _SLPSSetFrontProcessWithOptions(_ psn: inout ProcessSerialNumber, _ wid: UInt32, _ mode: UInt32) -> CGError @_silgen_name("SLPSPostEventRecordTo") @discardableResult func SLPSPostEventRecordTo(_ psn: inout ProcessSerialNumber, _ bytes: inout UInt8) -> CGError let kCPSUserGenerated: UInt32 = 0x200 // swiftlint:enable identifier_name /// Generic protocol for objects acting as windows in the system. protocol WindowType: Equatable { associatedtype Screen: ScreenType associatedtype WindowID: Codable, Hashable /// Returns the currently focused window of its type. static func currentlyFocused() -> Self? /** Attempt to initialize a window based on a Silica element. Many of the accessibility APIs handle elements directly, so we need a way to convert those elements into a general window type. This is not necessarily meaningful in all cases — tests, for example, may provide window types that do not correspond to actual elements. - Parameters: - element: The element representing a window. */ init?(element: SIAccessibilityElement?) /// Returns an opaque unique identifier for the window. func id() -> WindowID /// Returns the window's ID in the underlying window system. func cgID() -> CGWindowID /// Returns the window's current frame. func frame() -> CGRect /// Returns the screen, if any, that the window is currently on. func screen() -> Screen? /** Sets the frame of the window with an error threshold for what constitutes a new frame. The tolerance for error is necessary as for performance reasons we avoid performing unnecessary frame assignments, but some windows (e.g., Terminal's windows) have some constraints on their size such that `frame` and `window.frame()` will differ by some small amount even if `frame` has been applied before. We want to treat that frame as equivalent if it is close enough so that we get the performance benefit. - Parameters: - frame: The frame to apply. - threshold: The error tolerance for what constitutes a new frame. */ func setFrame(_ frame: CGRect, withThreshold threshold: CGSize) /// Whether or not the window is currently holding focus. func isFocused() -> Bool /// The process ID of the process that owns the window. func pid() -> pid_t /** The title of the window. - Note: Windows do not necessarily have titles so this can be `nil`. */ func title() -> String? /// Whether or not the window should actually be managed by Amethyst. func shouldBeManaged() -> Bool /// Whether or not the window should float by default. func shouldFloat() -> Bool /// Whether or not the window is currently active. func isActive() -> Bool /** Focuses the window. - Returns: `true` if the window was successfully focused, `false` otherwise. */ @discardableResult func focus() -> Bool @discardableResult func minimize() -> Bool /** Moves the window to a screen. This method takes into account the dimensions of the screen to ensure that the window actually fits onto it. - Parameters: - screen: The screen to move the window to. */ func moveScaled(to screen: Screen) /// Whether or not the window is currently on any screen. func isOnScreen() -> Bool /** Moves the window to a space. - Parameters: - space: The index of the space. */ func move(toSpace space: UInt) /** Moves the window to the space at an index. - Parameters: - space: The index of the space */ func move(toSpaceAtIndex space: UInt) /** Moves the window to a space. - Parameters: - spaceID: The id of the space. */ func move(toSpace spaceID: CGSSpaceID) } enum WindowDecodingError: Error { case idNotFound } /** Final subclass of the Silica `SIWindow`. A final class is necessary for satisfying the `focusedWindow()` requirement in the `WindowType` protocol. Otherwise, as `SIWindow` is not final, the type system does not know how to constrain `Self`. */ final class AXWindow: SIWindow {} /** Identifier for `AXWindow` objects. - Note: Decoding for this object is very inefficient. Use it sparingly. */ final class AXWindowID: Hashable, Codable { /// Coding keys. private enum CodingKeys: String, CodingKey { /// The pid of the process that owns the window. case pid /// The CoreGraphics id for the window. case windowID } private let window: AXWindow /// Equality for window IDs is based on the underlying CoreGraphics id and the owning pid, which (mostly) uniquely identifies a window. static func == (lhs: AXWindowID, rhs: AXWindowID) -> Bool { return lhs.window.pid() == rhs.window.pid() && lhs.window.windowID() == rhs.window.windowID() } func hash(into hasher: inout Hasher) { hasher.combine(window.pid()) hasher.combine(window.windowID()) } fileprivate init(window: AXWindow) { self.window = window } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let pid = try container.decode(pid_t.self, forKey: .pid) let windowID = try container.decode(CGWindowID.self, forKey: .windowID) guard let application = SIApplication(pid: pid) else { throw WindowDecodingError.idNotFound } let windows: [SIWindow] = application.windows() guard let window = windows.first(where: { $0.windowID() == windowID }) else { throw WindowDecodingError.idNotFound } self.window = AXWindow(axElement: window.axElementRef) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(window.pid(), forKey: .pid) try container.encode(window.windowID(), forKey: .windowID) } } extension AXWindowID: CustomStringConvertible { var description: String { return "\(window.title() ?? "unknown") (\(window.windowID()))" } } /// Conformance of `AXWindow` as an Amethyst window. extension AXWindow: WindowType { typealias Screen = AMScreen typealias WindowID = AXWindowID /** Returns the currently focused window. - Returns: The currently focused window as an `AXWindow`. */ static func currentlyFocused() -> AXWindow? { return SIWindow.focused().flatMap { AXWindow(axElement: $0.axElementRef) } } /** The Silica initializer is not failable because it can always assume it has a reference to an ax element. The window type in general does not make that assumption and thus has a failable initializer. This just ports one into the other. - Parameters: - element: The element representing a window. */ convenience init?(element: SIAccessibilityElement?) { guard let axElementRef = element?.axElementRef else { return nil } self.init(axElement: axElementRef) if string(forKey: "AXRole" as CFString) != "AXWindow" { return nil } } func id() -> WindowID { return AXWindowID(window: self) } func cgID() -> CGWindowID { return windowID() } func screen() -> AMScreen? { let nsScreen: NSScreen? = screen() return nsScreen.flatMap { AMScreen(screen: $0) } } func pid() -> pid_t { // Some window operations can surface elements owned by a helper process. // Use AXParent's PID when available so identity checks stay stable. return forKey("AXParent" as CFString)?.processIdentifier() ?? processIdentifier() } /** Whether or not the window should actually be managed by Amethyst. In this case the window must be movable and be a standard window. */ func shouldBeManaged() -> Bool { guard isMovable() else { return false } guard let subrole = string(forKey: kAXSubroleAttribute as CFString), subrole == kAXStandardWindowSubrole as String else { return false } return true } func shouldFloat() -> Bool { let userConfiguration = UserConfiguration.shared let frame = self.frame() let threshold = userConfiguration.smallWindowSize() if userConfiguration.floatSmallWindows() && frame.size.width < threshold && frame.size.height < threshold { return true } return false } func isFocused() -> Bool { guard let focused = AXWindow.currentlyFocused() else { return false } return isEqual(to: focused) } /** Focuses the window. This handles focusing and also moves the cursor to the window's frame if mouse-follows-focus is enabled. - Returns: `true` if the window was successfully focused, `false` otherwise. - Description: What a mess. See: https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468 */ @discardableResult override func focus() -> Bool { let pid = self.pid() var wid = self.cgID() var psn = ProcessSerialNumber() let status = GetProcessForPID(pid, &psn) guard status == noErr else { return false } var cgStatus = _SLPSSetFrontProcessWithOptions(&psn, wid, kCPSUserGenerated) guard cgStatus == .success else { return false } for byte in [0x01, 0x02] { var bytes = [UInt8](repeating: 0, count: 0xf8) bytes[0x04] = 0xF8 bytes[0x08] = UInt8(byte) bytes[0x3a] = 0x10 memcpy(&bytes[0x3c], &wid, MemoryLayout.size) memset(&bytes[0x20], 0xFF, 0x10) cgStatus = bytes.withUnsafeMutableBufferPointer { pointer in return SLPSPostEventRecordTo(&psn, &pointer.baseAddress!.pointee) } guard cgStatus == .success else { return false } } guard super.raise() else { return false } guard UserConfiguration.shared.mouseFollowsFocus() else { return true } let windowFrame = frame() let mouseCursorPoint = NSPoint(x: windowFrame.midX, y: windowFrame.midY) guard let mouseMoveEvent = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: mouseCursorPoint, mouseButton: .left) else { return true } mouseMoveEvent.flags = CGEventFlags(rawValue: 0) mouseMoveEvent.post(tap: CGEventTapLocation.cghidEventTap) return true } @discardableResult func minimize() -> Bool { super.minimize() return isWindowMinimized() } func moveScaled(to screen: Screen) { let screenFrame = screen.frameWithoutDockOrMenu() let currentFrame = frame() var scaledFrame = currentFrame if scaledFrame.width > screenFrame.width { scaledFrame.size.width = screenFrame.width } if scaledFrame.height > screenFrame.height { scaledFrame.size.height = screenFrame.height } if scaledFrame != currentFrame { setFrame(scaledFrame) } move(to: screen.screen) } func move(toSpaceAtIndex space: UInt) { super.move(toSpace: space) } func move(toSpace spaceID: CGSSpaceID) { } } extension AXWindow { override var description: String { return "\(super.description) (\(cgID()))" } } ================================================ FILE: Amethyst/Model/WindowsInformation.swift ================================================ // // WindowsInformation.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import ApplicationServices import Foundation import Silica extension CGRect { func approximatelyEqual(to otherRect: CGRect, within tolerance: CGRect) -> Bool { return abs(origin.x - otherRect.origin.x) < tolerance.origin.x && abs(origin.y - otherRect.origin.y) < tolerance.origin.y && abs(width - otherRect.width) < tolerance.width && abs(height - otherRect.height) < tolerance.height } } struct WindowsInformation { let ids: Set let descriptions: CGWindowsInfo? init?(windows: [Window]) { guard let descriptions = CGWindowsInfo(options: .optionOnScreenOnly, windowID: CGWindowID(0)) else { return nil } self.ids = Set(windows.map { $0.cgID() }) self.descriptions = descriptions } } extension WindowsInformation { // convert Window objects to CGWindowIDs. // additionally, return the full set of window descriptions (which is unsorted and may contain extra windows) fileprivate static func windowInformation(_ windows: [Window]) -> (IDs: Set, descriptions: [[String: AnyObject]]?) { let ids = Set(windows.map { $0.cgID() }) return (IDs: ids, descriptions: CGWindowsInfo(options: .optionOnScreenOnly, windowID: CGWindowID(0))?.descriptions) } fileprivate static func onScreenWindowsAtPoint(_ point: CGPoint, withIDs windowIDs: Set, withDescriptions windowDescriptions: [[String: AnyObject]]) -> [[String: AnyObject]] { var ret: [[String: AnyObject]] = [] // build a list of windows at this point for windowDescription in windowDescriptions { guard let windowID = (windowDescription[kCGWindowNumber as String] as? NSNumber).flatMap({ CGWindowID($0.intValue) }), windowIDs.contains(windowID) else { continue } // only consider windows with bounds guard let windowFrameDictionary = windowDescription[kCGWindowBounds as String] as? [String: Any] else { continue } // only consider window bounds that contain the given point let windowFrame = CGRect(dictionaryRepresentation: windowFrameDictionary as CFDictionary)! guard windowFrame.contains(point) else { continue } ret.append(windowDescription) } return ret } // if there are several windows at a given screen point, take the top one static func topWindowForScreenAtPoint(_ point: CGPoint, withWindows windows: [Window]) -> Window? { let (ids, maybeWindowDescriptions) = windowInformation(windows) guard let windowDescriptions = maybeWindowDescriptions, !windowDescriptions.isEmpty else { return nil } let windowsAtPoint = onScreenWindowsAtPoint(point, withIDs: ids, withDescriptions: windowDescriptions) guard !windowsAtPoint.isEmpty else { return nil } guard windowsAtPoint.count > 1 else { return windowInWindows(windows, withCGWindowDescription: windowsAtPoint[0]) } var windowToFocus: [String: AnyObject]? var minCount = windowDescriptions.count for windowDescription in windowsAtPoint { guard let windowID = windowDescription[kCGWindowNumber as String] as? NSNumber else { continue } guard let windowsAboveWindow = CGWindowsInfo(options: .optionOnScreenAboveWindow, windowID: windowID.uint32Value) else { continue } if windowsAboveWindow.descriptions.count < minCount { windowToFocus = windowDescription minCount = windowsAboveWindow.descriptions.count } } guard let windowDictionaryToFocus = windowToFocus else { return nil } return windowInWindows(windows, withCGWindowDescription: windowDictionaryToFocus) } // get the first window at a certain point, excluding one specific window from consideration static func alternateWindowForScreenAtPoint(_ point: CGPoint, withWindows windows: [Window], butNot ignoreWindow: Window?) -> Window? { // only consider windows on this screen let (ids, maybeWindowDescriptions) = windowInformation(windows) guard let windowDescriptions = maybeWindowDescriptions, !windowDescriptions.isEmpty else { return nil } let windowsAtPoint = onScreenWindowsAtPoint(point, withIDs: ids, withDescriptions: windowDescriptions) for windowDescription in windowsAtPoint { if let window = windowInWindows(windows, withCGWindowDescription: windowDescription) { if let ignored = ignoreWindow, window != ignored { return window } } } return nil } // find a window based on its window description within an array of Window objects static func windowInWindows(_ windows: [Window], withCGWindowDescription windowDescription: [String: AnyObject]) -> Window? { let potentialWindows = windows.filter { guard let windowOwnerPID = windowDescription[kCGWindowOwnerPID as String] as? NSNumber else { return false } guard windowOwnerPID.int32Value == $0.pid() else { return false } guard let boundsDictionary = windowDescription[kCGWindowBounds as String] as? [String: Any] else { return false } let windowFrame = CGRect(dictionaryRepresentation: boundsDictionary as CFDictionary)! guard windowFrame.equalTo($0.frame()) else { return false } return true } guard potentialWindows.count > 1 else { return potentialWindows.first } return potentialWindows.first { guard let describedTitle = windowDescription[kCGWindowName as String] as? String else { return false } let describedOwner = windowDescription[kCGWindowOwnerName as String] as? String let describedOwnedTitle = describedOwner.flatMap { "\(describedTitle) - \($0)" } return describedTitle == $0.title() || describedOwnedTitle == $0.title() } } } ================================================ FILE: Amethyst/Preferences/DebugPreferencesViewController.swift ================================================ // // DebugPreferencesViewController.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/9/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // import AppKit import Foundation class DebugPreferencesViewController: NSViewController {} ================================================ FILE: Amethyst/Preferences/DebugPreferencesViewController.xib ================================================ ================================================ FILE: Amethyst/Preferences/FloatingPreferencesViewController.swift ================================================ // // GeneralPreferencesViewController.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation class FloatingPreferencesViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate { private var floatingBundles: [FloatingBundle] { return arrayController.arrangedObjects as! [FloatingBundle] } @IBOutlet var floatingTableView: NSTableView! @IBOutlet var windowTitlesTableView: NSTableView! @IBOutlet var windowTitlesCoverView: NSView! @IBOutlet var arrayController: NSArrayController! override func awakeFromNib() { super.awakeFromNib() windowTitlesCoverView.wantsLayer = true windowTitlesCoverView.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.3).cgColor } override func viewWillAppear() { super.viewWillAppear() arrayController?.content = NSMutableArray(array: UserConfiguration.shared.floatingBundles()) arrayController?.setSelectionIndexes(IndexSet()) } @IBAction func addFloatingApplication(_ sender: NSButton) { let layoutMenu = NSMenu(title: "") let selectMenuItem = NSMenuItem(title: "Select from applications...", action: #selector(selectFloatingApplication(_:)), keyEquivalent: "") let manualMenuItem = NSMenuItem(title: "Manually enter identifier...", action: #selector(manuallyEnterFloatingApplication(_:)), keyEquivalent: "") layoutMenu.addItem(selectMenuItem) layoutMenu.addItem(manualMenuItem) let frame = sender.frame let menuOrigin = sender.superview!.convert(NSPoint(x: frame.origin.x, y: frame.origin.y + frame.size.height + 40), to: nil) let event = NSEvent.mouseEvent( with: NSEvent.EventType.leftMouseDown, location: menuOrigin, modifierFlags: [], timestamp: 0, windowNumber: sender.window!.windowNumber, context: sender.window!.graphicsContext, eventNumber: 0, clickCount: 1, pressure: 1 ) NSMenu.popUpContextMenu(layoutMenu, with: event!, for: sender) } @objc func selectFloatingApplication(_ sender: AnyObject) { let openPanel = NSOpenPanel() let applicationDirectories = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask) openPanel.canChooseFiles = true openPanel.canChooseDirectories = false openPanel.allowsMultipleSelection = true openPanel.allowedFileTypes = ["app"] openPanel.prompt = "Select" openPanel.directoryURL = applicationDirectories.first guard case openPanel.runModal() = NSApplication.ModalResponse.OK else { return } for applicationURL in openPanel.urls { guard let applicationBundleIdentifier = Bundle(url: applicationURL)?.bundleIdentifier else { continue } addFloatingApplicationBundleIdentifier(applicationBundleIdentifier) } } @objc func manuallyEnterFloatingApplication(_ sender: AnyObject) { guard let window = view.window else { return } let alert = NSAlert() let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) alert.accessoryView = field alert.icon = nil alert.messageText = "Application Bundle ID" alert.addButton(withTitle: "Add") alert.addButton(withTitle: "Cancel") alert.beginSheetModal(for: window) { [weak self] response in guard response == .alertFirstButtonReturn else { return } self?.addFloatingApplicationBundleIdentifier(field.stringValue) } } private func addFloatingApplicationBundleIdentifier(_ bundleIdentifier: String) { let floatingBundle = FloatingBundle(id: bundleIdentifier, windowTitles: []) arrayController.addObject(floatingBundle) UserConfiguration.shared.setFloatingBundles(self.floatingBundles) } @IBAction func removeFloatingApplication(_ sender: AnyObject) { guard let floatingTableView = floatingTableView else { return } guard floatingTableView.selectedRow < floatingBundles.count, floatingTableView.selectedRow != NSTableView.noRowSelectedIndex else { return } arrayController.remove(atArrangedObjectIndex: floatingTableView.selectedRow) UserConfiguration.shared.setFloatingBundles(self.floatingBundles) } @IBAction func addWindowTitle(_ sender: AnyObject) { guard let window = view.window else { return } let alert = NSAlert() let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) alert.accessoryView = field alert.icon = nil alert.messageText = "Window Title" alert.addButton(withTitle: "Add") alert.addButton(withTitle: "Cancel") alert.beginSheetModal(for: window) { [weak self] response in guard response == .alertFirstButtonReturn else { return } self?.addWindowTitleToSelectedApplication(field.stringValue) } } @IBAction func removeWindowTitle(_ sender: AnyObject) { let selection = arrayController.selection as AnyObject guard let id = selection.value(forKey: "id") as? String, let windowTitles = selection.value(forKey: "windowTitles") as? [String] else { return } guard windowTitlesTableView.selectedRow < windowTitles.count, windowTitlesTableView.selectedRow != NSTableView.noRowSelectedIndex else { return } let title = windowTitles[windowTitlesTableView.selectedRow] let updatedBundle = FloatingBundle(id: id, windowTitles: windowTitles.filter { $0 != title }) floatingBundles.firstIndex { $0.id == id }.flatMap { index in arrayController.remove(atArrangedObjectIndex: index) arrayController.insert(updatedBundle, atArrangedObjectIndex: index) } UserConfiguration.shared.setFloatingBundles(self.floatingBundles) } private func addWindowTitleToSelectedApplication(_ title: String) { let selection = arrayController.selection as AnyObject guard let id = selection.value(forKey: "id") as? String, let windowTitles = selection.value(forKey: "windowTitles") as? [String] else { return } let updatedBundle = FloatingBundle(id: id, windowTitles: windowTitles + [title]) floatingBundles.firstIndex { $0.id == id }.flatMap { index in arrayController.remove(atArrangedObjectIndex: index) arrayController.insert(updatedBundle, atArrangedObjectIndex: index) } UserConfiguration.shared.setFloatingBundles(self.floatingBundles) } } @objc(FloatingBlacklistIntBooleanTransformer) class FloatingBlacklistIntBooleanTransformer: ValueTransformer { override class func transformedValueClass() -> AnyClass { return NSNumber.self } override func transformedValue(_ value: Any?) -> Any? { guard let number = value as? Int else { return nil } switch number { case 0: return true case 1: return false default: return nil } } override func reverseTransformedValue(_ value: Any?) -> Any? { guard let boolean = value as? Bool else { return nil } return boolean ? 0 : 1 } } @objc(AllWindowsHiddenTitlesCountTransformer) class AllWindowsHiddenTitlesCountTransformer: ValueTransformer { override class func transformedValueClass() -> AnyClass { return NSNumber.self } override func transformedValue(_ value: Any?) -> Any? { guard let count = value as? Int else { return false } return count != 0 } } @objc(ApplicationSelectedTransformer) class ApplicationSelectedTransformer: ValueTransformer { override class func transformedValueClass() -> AnyClass { return NSNumber.self } override func transformedValue(_ value: Any?) -> Any? { guard let index = value as? Int else { return false } return index != NSNotFound } } @objc(NoApplicationSelectedTransformer) class NoApplicationSelectedTransformer: ValueTransformer { override class func transformedValueClass() -> AnyClass { return NSNumber.self } override func transformedValue(_ value: Any?) -> Any? { guard let index = value as? Int else { return false } return index == NSNotFound } } ================================================ FILE: Amethyst/Preferences/FloatingPreferencesViewController.xib ================================================ FloatingBlacklistIntBooleanTransformer Floating an application means that windows from that application will automatically be treated as floating. Windows can still manually be tiled or floated by toggling their floating state. AllWindowsHiddenTitlesCountTransformer NoApplicationSelectedTransformer ApplicationSelectedTransformer ================================================ FILE: Amethyst/Preferences/GeneralPreferencesViewController.swift ================================================ // // GeneralPreferencesViewController.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation import Silica class GeneralPreferencesViewController: NSViewController {} ================================================ FILE: Amethyst/Preferences/GeneralPreferencesViewController.xib ================================================ ================================================ FILE: Amethyst/Preferences/LayoutsPreferencesViewController.swift ================================================ // // LayoutsPreferencesViewController.swift // Amethyst // // Created by Ian Ynda-Hummel on 3/1/20. // Copyright © 2020 Ian Ynda-Hummel. All rights reserved. // import Cocoa class LayoutsPreferencesViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate { private var layoutKeys: [String] = [] @IBOutlet var layoutsTableView: NSTableView? @IBOutlet weak var openCustomLayoutsFolderButton: NSButton? @IBOutlet weak var relaunchButton: NSButton? override func awakeFromNib() { super.awakeFromNib() layoutsTableView?.dataSource = self layoutsTableView?.delegate = self layoutsTableView?.registerForDraggedTypes([.string]) } override func viewWillAppear() { super.viewWillAppear() layoutKeys = UserConfiguration.shared.layoutKeys() layoutsTableView?.reloadData() } @IBAction func addLayout(_ sender: NSButton) { let layoutMenu = NSMenu(title: "") for (layoutKey, layoutName) in LayoutType.availableLayoutStrings() { let menuItem = NSMenuItem(title: layoutName, action: #selector(addLayoutString(_:)), keyEquivalent: "") menuItem.representedObject = layoutKey menuItem.target = self menuItem.action = #selector(addLayoutString(_:)) layoutMenu.addItem(menuItem) } let frame = sender.frame let menuOrigin = sender.superview!.convert(NSPoint(x: frame.origin.x, y: frame.origin.y + frame.size.height + 40), to: nil) let event = NSEvent.mouseEvent( with: NSEvent.EventType.leftMouseDown, location: menuOrigin, modifierFlags: [], timestamp: 0, windowNumber: sender.window!.windowNumber, context: sender.window!.graphicsContext, eventNumber: 0, clickCount: 1, pressure: 1 ) NSMenu.popUpContextMenu(layoutMenu, with: event!, for: sender) } @IBAction func addLayoutString(_ sender: NSMenuItem) { guard let layoutKey: String = sender.representedObject as? String else { return } var layoutKeys = self.layoutKeys layoutKeys.append(layoutKey) self.layoutKeys = layoutKeys UserConfiguration.shared.setLayoutKeys(self.layoutKeys) relaunchButton?.isEnabled = true layoutsTableView?.reloadData() } @IBAction func removeLayout(_ sender: AnyObject) { guard let selectedRow = layoutsTableView?.selectedRow, selectedRow < self.layoutKeys.count, selectedRow != NSTableView.noRowSelectedIndex else { return } layoutKeys.remove(at: selectedRow) UserConfiguration.shared.setLayoutKeys(layoutKeys) relaunchButton?.isEnabled = true layoutsTableView?.reloadData() } func numberOfRows(in tableView: NSTableView) -> Int { return layoutKeys.count } func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { guard row > -1 else { return nil } return LayoutType.layoutNameForKey(layoutKeys[row]) } @IBAction func relaunch(_ sender: AnyObject) { AppManager.relaunch() } @IBAction func openCustomLayoutsFolder(_ sender: AnyObject) { do { let layoutsDirectory = try FileManager.default.layoutsDirectory() NSWorkspace.shared.open(layoutsDirectory) } catch { // Handle error - could show an alert or log the error log.error("Failed to open layouts directory: \(error)") } } func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool { if let dragData = info.draggingPasteboard.data(forType: .string), let rowString = String(bytes: dragData, encoding: .utf8), let oldRow = Int(rowString) { layoutKeys.move(from: oldRow, to: oldRow < row ? row-1 : row) UserConfiguration.shared.setLayoutKeys(self.layoutKeys) layoutsTableView?.reloadData() return true } return false } func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation { if dropOperation == .above { return .move } return [] } func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? { let item = NSPasteboardItem() item.setString(String(row), forType: .string) return item } } private extension Array { mutating func move(from oldIndex: Index, to newIndex: Index) { if oldIndex == newIndex { return } if abs(newIndex - oldIndex) == 1 { return self.swapAt(oldIndex, newIndex) } self.insert(self.remove(at: oldIndex), at: newIndex) } } ================================================ FILE: Amethyst/Preferences/LayoutsPreferencesViewController.xib ================================================ ================================================ FILE: Amethyst/Preferences/MousePreferencesViewController.swift ================================================ // // MousePreferencesViewController.swift // Amethyst // // Created by Ian Ynda-Hummel on 2/29/20. // Copyright © 2020 Ian Ynda-Hummel. All rights reserved. // import Cocoa class MousePreferencesViewController: NSViewController {} ================================================ FILE: Amethyst/Preferences/MousePreferencesViewController.xib ================================================ ================================================ FILE: Amethyst/Preferences/ShortcutsPreferencesListItemView.swift ================================================ // // ShortcutsPreferencesListItemView.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Cartography import Cocoa import Foundation import KeyboardShortcuts class ShortcutsPreferencesListItemView: NSView { private(set) var nameLabel: NSTextField? override init(frame frameRect: NSRect) { super.init(frame: frameRect) let label = NSTextField() label.isBezeled = false label.isEditable = false label.stringValue = "" label.backgroundColor = NSColor.clear label.sizeToFit() addSubview(label) constrain(label, self) { label, view in label.centerY == view.centerY label.left == view.left + 8 } self.nameLabel = label } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setShortcutName(name: KeyboardShortcuts.Name) { let shortcutView = KeyboardShortcuts.RecorderCocoa(for: name) addSubview(shortcutView) constrain(shortcutView, self) { shortcutView, view in shortcutView.centerY == view.centerY shortcutView.right == view.right - 16 } } } ================================================ FILE: Amethyst/Preferences/ShortcutsPreferencesViewController.swift ================================================ // // ShortcutsPreferencesViewController.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation import KeyboardShortcuts import Silica class ShortcutsPreferencesViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate { private var hotKeyNameToDefaultsKey: [[String]] = [] @IBOutlet var tableView: NSTableView? override func awakeFromNib() { tableView?.dataSource = self tableView?.delegate = self } override func viewWillAppear() { super.viewWillAppear() hotKeyNameToDefaultsKey = HotKeyManager.hotKeyNameToDefaultsKey() tableView?.reloadData() } func numberOfRows(in tableView: NSTableView) -> Int { return hotKeyNameToDefaultsKey.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let frame = NSRect(x: 0, y: 0, width: tableView.frame.size.width, height: 30) let shortcutItemView = ShortcutsPreferencesListItemView(frame: frame) let name = hotKeyNameToDefaultsKey[row][0] let key = hotKeyNameToDefaultsKey[row][1] shortcutItemView.nameLabel?.stringValue = name shortcutItemView.setShortcutName(name: KeyboardShortcuts.Name(key)) return shortcutItemView } func selectionShouldChange(in tableView: NSTableView) -> Bool { return false } } ================================================ FILE: Amethyst/Preferences/ShortcutsPreferencesViewController.xib ================================================ ================================================ FILE: Amethyst/Preferences/UserConfiguration.swift ================================================ // // UserConfiguration.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/8/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation import SwiftyJSON import Yams enum DefaultFloat: Equatable { case floating case notFloating fileprivate static func from(_ bool: Bool) -> DefaultFloat { return bool ? .floating : .notFloating } } protocol ConfigurationStorage { func object(forKey key: ConfigurationKey) -> Any? func array(forKey key: ConfigurationKey) -> [Any]? func bool(forKey key: ConfigurationKey) -> Bool func float(forKey key: ConfigurationKey) -> Float func stringArray(forKey key: ConfigurationKey) -> [String]? func set(_ value: Any?, forKey key: ConfigurationKey) func set(_ value: Bool, forKey key: ConfigurationKey) } extension UserDefaults: ConfigurationStorage { func object(forKey key: ConfigurationKey) -> Any? { return object(forKey: key.rawValue) } func array(forKey key: ConfigurationKey) -> [Any]? { return array(forKey: key.rawValue) } func bool(forKey key: ConfigurationKey) -> Bool { return bool(forKey: key.rawValue) } func float(forKey key: ConfigurationKey) -> Float { return float(forKey: key.rawValue) } func stringArray(forKey key: ConfigurationKey) -> [String]? { return stringArray(forKey: key.rawValue) } func set(_ value: Any?, forKey key: ConfigurationKey) { set(value, forKey: key.rawValue) } func set(_ value: Bool, forKey key: ConfigurationKey) { set(value, forKey: key.rawValue) } } enum ConfigurationKey: String { case layouts = "layouts" case commandMod = "mod" case commandKey = "key" case mod1 = "mod1" case mod2 = "mod2" case mod3 = "mod3" case mod4 = "mod4" case windowMargins = "window-margins" case smartWindowMargins = "smart-window-margins" case windowMarginSize = "window-margin-size" case windowMinimumHeight = "window-minimum-height" case windowMinimumWidth = "window-minimum-width" case windowMaxCount = "window-max-count" case floatingBundleIdentifiers = "floating" case floatingBundleIdentifiersIsBlacklist = "floating-is-blacklist" case ignoreMenuBar = "ignore-menu-bar" case floatSmallWindows = "float-small-windows" case smallWindowSize = "small-window-size" case mouseFollowsFocus = "mouse-follows-focus" case focusFollowsMouse = "focus-follows-mouse" case mouseSwapsWindows = "mouse-swaps-windows" case mouseResizesWindows = "mouse-resizes-windows" case layoutHUD = "enables-layout-hud" case layoutHUDOnSpaceChange = "enables-layout-hud-on-space-change" case windowCountHUD = "enables-window-count-hud" case useCanaryBuild = "use-canary-build" case newWindowsToMain = "new-windows-to-main" case followSpaceThrownWindows = "follow-space-thrown-windows" case windowResizeStep = "window-resize-step" case screenPaddingLeft = "screen-padding-left" case screenPaddingRight = "screen-padding-right" case screenPaddingTop = "screen-padding-top" case screenPaddingBottom = "screen-padding-bottom" case debugLayoutInfo = "debug-layout-info" case restoreLayoutsOnLaunch = "restore-layouts-on-launch" case disablePaddingOnBuiltinDisplay = "disable-padding-on-builtin-display" case hideMenuBarIcon = "hide-menu-bar-icon" } extension ConfigurationKey: CaseIterable {} enum CommandKey: String { case cycleLayoutForward = "cycle-layout" case cycleLayoutBackward = "cycle-layout-backward" case shrinkMain = "shrink-main" case expandMain = "expand-main" case increaseMain = "increase-main" case decreaseMain = "decrease-main" case command1 = "command1" case command2 = "command2" case command3 = "command3" case command4 = "command4" case focusCCW = "focus-ccw" case focusCW = "focus-cw" case focusMain = "focus-main" case focusScreenCCW = "focus-screen-ccw" case focusScreenCW = "focus-screen-cw" case swapScreenCCW = "swap-screen-ccw" case swapScreenCW = "swap-screen-cw" case swapCCW = "swap-ccw" case swapCW = "swap-cw" case swapMain = "swap-main" case throwSpacePrefix = "throw-space" case focusScreenPrefix = "focus-screen" case throwScreenPrefix = "throw-screen" case throwSpaceLeft = "throw-space-left" case throwSpaceRight = "throw-space-right" case toggleFloat = "toggle-float" case displayCurrentLayout = "display-current-layout" case toggleTiling = "toggle-tiling" case enableTiling = "enable-tiling" case disableTiling = "disable-tiling" case reevaluateWindows = "reevaluate-windows" case toggleFocusFollowsMouse = "toggle-focus-follows-mouse" case relaunchAmethyst = "relaunch-amethyst" case increaseWindowMaxCount = "increase-window-max-count" case decreaseWindowMaxCount = "decrease-window-max-count" } protocol UserConfigurationDelegate: AnyObject { func configurationGlobalTilingDidChange(_ userConfiguration: UserConfiguration) func configurationAccessibilityPermissionsDidChange(_ userConfiguration: UserConfiguration) } class FloatingBundle: NSObject { @objc dynamic let id: String @objc dynamic let windowTitles: [String] init(id: String, windowTitles: [String]) { self.id = id self.windowTitles = windowTitles } override func isEqual(_ object: Any?) -> Bool { guard let other = object as? FloatingBundle else { return false } return other.id == id && other.windowTitles == windowTitles } func encoded() -> Any { return [ "id": id, "window-titles": windowTitles ] } static func from(_ object: Any) -> FloatingBundle? { if let id = object as? String { return FloatingBundle(id: id, windowTitles: []) } else if let dict = object as? [String: Any] { var json = JSON(dict) if let id = json["id"].string, let windowTitles = json["window-titles"].arrayObject as? [String] { return FloatingBundle(id: id, windowTitles: windowTitles) } guard let key = dict.keys.first, dict.count == 1 else { return nil } json = json[key] if let windowTitles = json["window-titles"].arrayObject as? [String] { return FloatingBundle(id: key, windowTitles: windowTitles) } return nil } else { return nil } } } class UserConfiguration: NSObject { static let shared = UserConfiguration() private let storage: ConfigurationStorage weak var delegate: UserConfigurationDelegate? var tilingEnabled = true { didSet { delegate?.configurationGlobalTilingDidChange(self) } } var hasAccessibilityPermissions = true { didSet { delegate?.configurationAccessibilityPermissionsDidChange(self) } } var configurationYAML: [String: Any]? var configurationJSON: JSON? var defaultConfiguration: JSON? var modifier1: AMModifierFlags? var modifier2: AMModifierFlags? var modifier3: AMModifierFlags? var modifier4: AMModifierFlags? init(storage: ConfigurationStorage) { self.storage = storage } override convenience init() { self.init(storage: UserDefaults.standard) } private func configurationValueForKey(_ key: ConfigurationKey, fallbackToDefault: Bool = true) -> T? { return configurationValue(forKeyValue: key.rawValue, fallbackToDefault: fallbackToDefault) } private func configurationValue(forKeyValue keyValue: String, fallbackToDefault: Bool = true) -> T? { if let yamlValue = configurationYAML?[keyValue] { if yamlValue is NSNull { return nil } else { return yamlValue as? T } } if let jsonValue = configurationJSON?[keyValue], jsonValue.exists(), jsonValue.error == nil { return jsonValue.rawValue as? T } if fallbackToDefault { return defaultConfiguration![keyValue].rawValue as? T } return nil } func modifierFlagsForStrings(_ modifierStrings: [String]) -> AMModifierFlags { var flags: NSEvent.ModifierFlags = [] for modifierString in modifierStrings { switch modifierString { case "option": flags.insert(.option) case "shift": flags.insert(.shift) case "control": flags.insert(.control) case "command": flags.insert(.command) default: log.warning("Unrecognized modifier string: \(modifierString)") } } return flags } func load() { let hasAccessibilityPermissions = confirmAccessibilityPermissions() if self.hasAccessibilityPermissions != hasAccessibilityPermissions { self.hasAccessibilityPermissions = hasAccessibilityPermissions } loadConfigurationFile() loadConfiguration() } func loadConfiguration() { for key in ConfigurationKey.allCases { let value: Any? = configurationValueForKey(key, fallbackToDefault: false) let defaultValue = defaultConfiguration?[key.rawValue] let existingValue = storage.object(forKey: key) let hasLocalConfigurationValue = value != nil let hasDefaultConfigurationValue = (defaultValue != nil && defaultValue?.error == nil) let hasExistingValue = (existingValue != nil) guard hasLocalConfigurationValue || (hasDefaultConfigurationValue && !hasExistingValue) else { continue } storage.set(hasLocalConfigurationValue ? value : defaultValue?.rawValue, forKey: key) } } private func yamlForConfig(at path: String) -> [String: Any]? { guard FileManager.default.fileExists(atPath: path, isDirectory: nil) else { return nil } let configPath = URL(fileURLWithPath: path) guard let string = try? String(contentsOf: configPath) else { return nil } do { let yaml = try Yams.load(yaml: string) return yaml as? [String: Any] } catch { log.debug(error) return nil } } private func jsonForConfig(at path: String) -> JSON? { guard FileManager.default.fileExists(atPath: path, isDirectory: nil) else { return nil } let configPath = URL(fileURLWithPath: path) guard let data = try? Data(contentsOf: configPath) else { return nil } return try? JSON(data: data) } private func loadConfigurationFile() { let xdgConfigPath = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"] ?? NSHomeDirectory().appending("/.config") let amethystXDGConfigPath = xdgConfigPath.appending("/amethyst") let amethystYAMLConfigPath = NSHomeDirectory().appending("/.amethyst.yml") let amethystJSONConfigPath = NSHomeDirectory().appending("/.amethyst") let defaultAmethystConfigPath = Bundle.main.path(forResource: "default", ofType: "amethyst") var isDirectory: ObjCBool = false /** Prioritiy order for config files: 1. yml in home dir 2. yml in xdg path 3. json in home dir 4. default json */ if FileManager.default.fileExists(atPath: amethystYAMLConfigPath, isDirectory: &isDirectory) { configurationYAML = yamlForConfig(at: amethystYAMLConfigPath) if configurationYAML == nil { log.error("error loading configuration as yaml") let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "Error loading configuration" alert.runModal() } } else if FileManager.default.fileExists(atPath: amethystXDGConfigPath, isDirectory: &isDirectory) { configurationYAML = yamlForConfig(at: isDirectory.boolValue ? amethystXDGConfigPath.appending("/amethyst.yml") : amethystXDGConfigPath) if configurationYAML == nil { log.error("error loading configuration as yaml") let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "Error loading configuration" alert.runModal() } } else if FileManager.default.fileExists(atPath: amethystJSONConfigPath, isDirectory: &isDirectory) { configurationJSON = jsonForConfig(at: amethystJSONConfigPath) if configurationJSON == nil { log.error("error loading configuration as json") let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "Error loading configuration" alert.runModal() } } defaultConfiguration = jsonForConfig(at: defaultAmethystConfigPath ?? "") if defaultConfiguration == nil { log.error("error loading default configuration") let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "Error loading default configuration" alert.runModal() } let mod1Strings: [String] = configurationValueForKey(.mod1) ?? [] let mod2Strings: [String] = configurationValueForKey(.mod2) ?? [] let mod3Strings: [String]? = configurationValueForKey(.mod3) let mod4Strings: [String]? = configurationValueForKey(.mod4) modifier1 = modifierFlagsForStrings(mod1Strings) modifier2 = modifierFlagsForStrings(mod2Strings) if modifier1 == nil || modifier1!.isEmpty { log.error("error loading a mod1") let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "Error loading mod1" alert.runModal() } if modifier2 == nil || modifier2!.isEmpty { log.error("error loading a mod2") let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "Error loading mod2" alert.runModal() } if let mod3Strings = mod3Strings { modifier3 = modifierFlagsForStrings(mod3Strings) } if let mod4Strings = mod4Strings { modifier4 = modifierFlagsForStrings(mod4Strings) } } static func constructLayoutKeyString(_ layoutKey: String) -> String { return "select-\(layoutKey)-layout" } func constructCommand(for hotKeyRegistrar: HotKeyRegistrar, commandKey: String, handler: @escaping HotKeyHandler) { var override = false var command: [String: String]? = configurationValue(forKeyValue: commandKey, fallbackToDefault: false) if command != nil { override = true } else if let enabled: Bool = configurationValue(forKeyValue: commandKey, fallbackToDefault: false), !enabled { override = true command = nil } else { let mod1: [String]? = configurationValueForKey(.mod1, fallbackToDefault: false) let mod2: [String]? = configurationValueForKey(.mod2, fallbackToDefault: false) let mod3: [String]? = configurationValueForKey(.mod3, fallbackToDefault: false) let mod4: [String]? = configurationValueForKey(.mod4, fallbackToDefault: false) if mod1 != nil || mod2 != nil || mod3 != nil || mod4 != nil { override = true } command = defaultConfiguration?[commandKey].rawValue as? [String: String] } let commandKeyString = command?[ConfigurationKey.commandKey.rawValue] let commandModifierString = command?[ConfigurationKey.commandMod.rawValue] var commandFlags: AMModifierFlags? if let modifierString = commandModifierString { switch modifierString { case "mod1": commandFlags = modifier1 case "mod2": commandFlags = modifier2 case "mod3": commandFlags = modifier3 case "mod4": commandFlags = modifier4 default: log.warning("Unknown modifier string: \(modifierString)") return } } let injectedHandler: () -> Void = { [weak self] in guard let `self` = self else { return } let hasAccessibilityPermissions = self.confirmAccessibilityPermissions() if self.hasAccessibilityPermissions != hasAccessibilityPermissions { self.hasAccessibilityPermissions = hasAccessibilityPermissions } guard hasAccessibilityPermissions else { return } DispatchQueue.global(qos: .userInitiated).async { handler() } } hotKeyRegistrar.registerHotKey( with: commandKeyString, modifiers: commandFlags, handler: injectedHandler, defaultsKey: commandKey, override: override ) } func hasCustomConfiguration() -> Bool { return configurationYAML != nil || configurationJSON != nil } private func modifierFlagsForModifierString(_ modifierString: String) -> AMModifierFlags { switch modifierString { case "mod1": return modifier1! case "mod2": return modifier2! case "mod3": return modifier3! case "mod4": return modifier4! default: log.warning("Unknown modifier string: \(modifierString)") return modifier1! } } func layoutKeys() -> [String] { let layoutKeys = storage.array(forKey: .layouts) as? [String] return layoutKeys ?? [] } func setLayoutKeys(_ layoutKeys: [String]) { storage.set(layoutKeys as Any?, forKey: .layouts) } func runningApplication(_ runningApplication: BundleIdentifiable, byDefaultFloatsForTitle title: String?) -> Reliable { let useIdentifiersAsBlacklist = floatingBundleIdentifiersIsBlacklist() // If the application is in the floating list we need to continue to check title // Otherwise // - Blacklist means not floating // - Whitelist menas floating guard let floatingBundle = runningApplicationFloatingBundle(runningApplication) else { return .reliable(DefaultFloat.from(!useIdentifiersAsBlacklist)) } // If the window list is empty then all windows are included in the list // - Blacklist means floating // - Whitelist means not floating if floatingBundle.windowTitles.isEmpty { return .reliable(DefaultFloat.from(useIdentifiersAsBlacklist)) } // If the title is `nil` then we cannot make a determination so we fall back to the default. However, we have to treat this value as unreliable as the window could have just been created and be in the process of loading. guard let title = title else { return .unreliable(DefaultFloat.from(!useIdentifiersAsBlacklist)) } // If the title matches it is included // - Blacklist means floating // - Whitelist means not floating if floatingBundle.windowTitles.contains(where: { windowTitle in if title.range(of: windowTitle, options: .regularExpression) != nil { return true } else { return false } }) { return .reliable(DefaultFloat.from(useIdentifiersAsBlacklist)) } // Otherwise the window is not included // - Blacklist means not floating // - Whitelist means floating let defaultFloat = DefaultFloat.from(!useIdentifiersAsBlacklist) // If the title is empty the window could have just been created and in the process of loading. Our float determination could still be correct, but to account for the potential change we mark it as unreliable. if title.isEmpty { return .unreliable(defaultFloat) } return .reliable(defaultFloat) } func runningApplicationFloatingBundle(_ runningApplication: BundleIdentifiable) -> FloatingBundle? { let floatingBundles = self.floatingBundles() let bundleIdentifier = runningApplication.bundleIdentifier ?? "" for floatingBundle in floatingBundles { if floatingBundle.id.contains("*") { do { let pattern = floatingBundle.id .replacingOccurrences(of: ".", with: "\\.") .replacingOccurrences(of: "*", with: ".*") let regex = try NSRegularExpression(pattern: "^\(pattern)$", options: []) let fullRange = NSRange(location: 0, length: bundleIdentifier.count) if regex.firstMatch(in: bundleIdentifier, options: [], range: fullRange) != nil { return floatingBundle } } catch { continue } } else { if floatingBundle.id == runningApplication.bundleIdentifier { return floatingBundle } } } return nil } func ignoreMenuBar() -> Bool { return storage.bool(forKey: .ignoreMenuBar) } func floatSmallWindows() -> Bool { return storage.bool(forKey: .floatSmallWindows) } func smallWindowSize() -> CGFloat { let size = CGFloat(storage.float(forKey: .smallWindowSize)) return size > 0 ? size : 500 } func mouseFollowsFocus() -> Bool { return storage.bool(forKey: .mouseFollowsFocus) } func focusFollowsMouse() -> Bool { return storage.bool(forKey: .focusFollowsMouse) } func toggleFocusFollowsMouse() { storage.set(!focusFollowsMouse(), forKey: .focusFollowsMouse) } func mouseSwapsWindows() -> Bool { return storage.bool(forKey: .mouseSwapsWindows) } func mouseResizesWindows() -> Bool { return storage.bool(forKey: .mouseResizesWindows) } func enablesLayoutHUD() -> Bool { return storage.bool(forKey: .layoutHUD) } func enablesLayoutHUDOnSpaceChange() -> Bool { return storage.bool(forKey: .layoutHUDOnSpaceChange) } func enablesWindowCountHUD() -> Bool { return storage.bool(forKey: .windowCountHUD) } func useCanaryBuild() -> Bool { return storage.bool(forKey: .useCanaryBuild) } func windowMarginSize() -> CGFloat { return CGFloat(storage.float(forKey: .windowMarginSize)) } func windowMargins() -> Bool { if !storage.bool(forKey: .windowMargins) { return false } // if smartWindowMargins is not enabled, enable window margins if !smartWindowMargins() { return true } // if smartWindowMargins is enabled, enabled window margins if there are more than one visible windows on screen let options = CGWindowListOption(arrayLiteral: .excludeDesktopElements, .optionOnScreenOnly) let windowsListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0)) let infoList = windowsListInfo as! [[String: Any]] let visibleWindows = infoList.filter { $0["kCGWindowLayer"] as! Int == 0 } return visibleWindows.count > 1 } func smartWindowMargins() -> Bool { return storage.bool(forKey: .smartWindowMargins) } func windowMinimumHeight() -> CGFloat { return CGFloat(storage.float(forKey: .windowMinimumHeight)) } func windowMinimumWidth() -> CGFloat { return CGFloat(storage.float(forKey: .windowMinimumWidth)) } func windowMaxCount() -> Int? { let int = Int(storage.float(forKey: .windowMaxCount)) return int == 0 ? nil : int } private func setConfigurationValueWithKVO(_ value: Any?, forKey key: ConfigurationKey) { if let userDefaults = storage as? UserDefaults { userDefaults.willChangeValue(forKey: key.rawValue) userDefaults.set(value, forKey: key) userDefaults.didChangeValue(forKey: key.rawValue) } else { storage.set(value, forKey: key) } } func increaseWindowMaxCount() { let currentCount = windowMaxCount() ?? 0 let newCount = currentCount + 1 setConfigurationValueWithKVO(Float(newCount), forKey: .windowMaxCount) } func decreaseWindowMaxCount() { let currentCount = windowMaxCount() ?? 1 let newCount = max(0, currentCount - 1) setConfigurationValueWithKVO(Float(newCount), forKey: .windowMaxCount) } func windowResizeStep() -> CGFloat { return CGFloat(storage.float(forKey: .windowResizeStep) / 100.0) } func screenPaddingTop() -> CGFloat { return CGFloat(storage.float(forKey: .screenPaddingTop)) } func screenPaddingBottom() -> CGFloat { return CGFloat(storage.float(forKey: .screenPaddingBottom)) } func screenPaddingLeft() -> CGFloat { return CGFloat(storage.float(forKey: .screenPaddingLeft)) } func screenPaddingRight() -> CGFloat { return CGFloat(storage.float(forKey: .screenPaddingRight)) } func floatingBundleIdentifiersIsBlacklist() -> Bool { guard storage.object(forKey: .floatingBundleIdentifiersIsBlacklist) != nil else { return true } return storage.bool(forKey: .floatingBundleIdentifiersIsBlacklist) } func floatingBundles() -> [FloatingBundle] { guard let floatingBundles = storage.array(forKey: .floatingBundleIdentifiers) else { return [] } return floatingBundles.compactMap { FloatingBundle.from($0) } } func setFloatingBundles(_ floatingBundles: [FloatingBundle]) { storage.set(floatingBundles.map { $0.encoded() }, forKey: .floatingBundleIdentifiers) } func sendNewWindowsToMainPane() -> Bool { return storage.bool(forKey: .newWindowsToMain) } func disablePaddingOnBuiltinDisplay() -> Bool { return storage.bool(forKey: .disablePaddingOnBuiltinDisplay) } func followWindowsThrownBetweenSpaces() -> Bool { return storage.bool(forKey: .followSpaceThrownWindows) } func restoreLayoutsOnLaunch() -> Bool { return storage.bool(forKey: .restoreLayoutsOnLaunch) } func hideMenuBarIcon() -> Bool { return storage.bool(forKey: .hideMenuBarIcon) } } extension UserConfiguration { @discardableResult func confirmAccessibilityPermissions() -> Bool { let options = [ kAXTrustedCheckOptionPrompt.takeRetainedValue() as String: true ] return AXIsProcessTrustedWithOptions(options as CFDictionary) } } ================================================ FILE: Amethyst/View/LayoutNameWindow.swift ================================================ // // LayoutNameWindow.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import Cocoa import Foundation import QuartzCore class LayoutNameWindow: NSWindow { @IBOutlet weak var layoutNameField: NSTextField? @IBOutlet weak var layoutDescriptionLabel: NSTextField? @IBOutlet override var contentView: NSView? { didSet { contentView?.wantsLayer = true contentView?.layer?.frame = NSRectToCGRect(contentView!.frame) contentView?.layer?.cornerRadius = 20.0 contentView?.layer?.masksToBounds = true contentView?.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.75).cgColor } } @IBOutlet var containerView: NSView? override func awakeFromNib() { super.awakeFromNib() isOpaque = false ignoresMouseEvents = true backgroundColor = NSColor.clear level = .floating } // Display custom notification with dynamic sizing func displayNotification(title: String, description: String) { // layoutDescriptionLabel?.isHidden = false layoutNameField?.stringValue = title layoutDescriptionLabel?.stringValue = description // Calculate size needed for both name and description let longerText = title.count > description.count ? title : description resizeToFitText(text: longerText) } // Dynamic window resizing based on text content private func resizeToFitText(text: String) { guard let textField = layoutNameField else { return } // Calculate text width with current font let font = textField.font ?? NSFont.systemFont(ofSize: 20) let textSize = text.size(withAttributes: [.font: font]) // Add padding (40px on each side + extra space) let minWidth: CGFloat = 200 let maxWidth: CGFloat = 500 let padding: CGFloat = 80 let calculatedWidth = textSize.width + padding // Constrain width between min and max let newWidth = max(minWidth, min(maxWidth, calculatedWidth)) // Keep the same height let currentFrame = frame let newFrame = NSRect( x: currentFrame.origin.x, y: currentFrame.origin.y, width: newWidth, height: currentFrame.height ) setFrame(newFrame, display: true, animate: false) // Update content view layer frame contentView?.layer?.frame = NSRectToCGRect(contentView!.frame) } } ================================================ FILE: Amethyst/View/LayoutNameWindow.xib ================================================ NSNegateBoolean ================================================ FILE: Amethyst/View/LayoutNameWindowController.swift ================================================ // // LayoutNameWIndowController.swift // Amethyst // // Created by Ian Ynda-Hummel on 1/16/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // import AppKit class LayoutNameWindowController: NSWindowController { } ================================================ FILE: Amethyst/View/PreferencesWindow.swift ================================================ // // PreferencesWindow.swift // Amethyst // // Created by Ian Ynda-Hummel on 7/19/17. // Copyright © 2017 Ian Ynda-Hummel. All rights reserved. // import AppKit class PreferencesWindowController: NSWindowController { override func awakeFromNib() { super.awakeFromNib() window?.title = "" guard let firstItem = window?.toolbar?.items.first else { return } window?.toolbar?.selectedItemIdentifier = firstItem.itemIdentifier selectPane(firstItem) } @IBAction func selectPane(_ sender: NSToolbarItem) { switch sender.itemIdentifier.rawValue { case "general": contentViewController = GeneralPreferencesViewController() case "layouts": contentViewController = LayoutsPreferencesViewController() case "shortcuts": contentViewController = ShortcutsPreferencesViewController() case "mouse": contentViewController = MousePreferencesViewController() case "floating": contentViewController = FloatingPreferencesViewController() case "debug": contentViewController = DebugPreferencesViewController() default: break } } } class PreferencesWindow: NSWindow { @IBOutlet var closeMenuItem: NSMenuItem? override func keyDown(with event: NSEvent) { super.keyDown(with: event) guard let closeMenuItem = closeMenuItem else { return } let eventModifierMask = event.modifierFlags.intersection(.deviceIndependentFlagsMask) guard closeMenuItem.keyEquivalentModifierMask == eventModifierMask && closeMenuItem.keyEquivalent == event.characters else { return } close() } } ================================================ FILE: Amethyst/default.amethyst ================================================ { "layouts": [ "tall", "wide", "fullscreen", "column" ], "mod1": [ "option", "shift" ], "mod2": [ "option", "shift", "control" ], "cycle-layout": { "mod": "mod1", "key": "space" }, "cycle-layout-backward": { "mod": "mod2", "key": "space" }, "select-tall-layout": { "mod": "mod1", "key": "a" }, "select-wide-layout": { "mod": "mod1", "key": "s" }, "select-fullscreen-layout": { "mod": "mod1", "key": "d" }, "select-column-layout": { "mod": "mod1", "key": "f" }, "focus-screen-ccw": { "mod": "mod1", "key": "p" }, "focus-screen-cw": { "mod": "mod1", "key": "n" }, "focus-screen-1": { "mod": "mod1", "key": "w" }, "focus-screen-2": { "mod": "mod1", "key": "e" }, "focus-screen-3": { "mod": "mod1", "key": "r" }, "focus-screen-4": { "mod": "mod1", "key": "q" }, "focus-screen-5": { "mod": "mod1", "key": "g" }, "throw-screen-1": { "mod": "mod2", "key": "w" }, "throw-screen-2": { "mod": "mod2", "key": "e" }, "throw-screen-3": { "mod": "mod2", "key": "r" }, "throw-screen-4": { "mod": "mod2", "key": "q" }, "throw-screen-5": { "mod": "mod2", "key": "g" }, "shrink-main": { "mod": "mod1", "key": "h" }, "expand-main": { "mod": "mod1", "key": "l" }, "increase-main": { "mod": "mod1", "key": "," }, "decrease-main": { "mod": "mod1", "key": "." }, "focus-ccw": { "mod": "mod1", "key": "j" }, "focus-cw": { "mod": "mod1", "key": "k" }, "focus-main": { "mod": "mod1", "key": "m" }, "swap-screen-ccw": { "mod": "mod2", "key": "h" }, "swap-screen-cw": { "mod": "mod2", "key": "l" }, "swap-ccw": { "mod": "mod2", "key": "j" }, "swap-cw": { "mod": "mod2", "key": "k" }, "swap-main": { "mod": "mod1", "key": "enter" }, "throw-space-1": { "mod": "mod2", "key": "1" }, "throw-space-2": { "mod": "mod2", "key": "2" }, "throw-space-3": { "mod": "mod2", "key": "3" }, "throw-space-4": { "mod": "mod2", "key": "4" }, "throw-space-5": { "mod": "mod2", "key": "5" }, "throw-space-6": { "mod": "mod2", "key": "6" }, "throw-space-7": { "mod": "mod2", "key": "7" }, "throw-space-8": { "mod": "mod2", "key": "8" }, "throw-space-9": { "mod": "mod2", "key": "9" }, "throw-space-10": { "mod": "mod2", "key": "0" }, "throw-space-left": { "mod": "mod2", "key": "left" }, "throw-space-right": { "mod": "mod2", "key": "right" }, "toggle-float": { "mod": "mod1", "key": "t" }, "toggle-tiling": { "mod": "mod2", "key": "t" }, "enable-tiling": false, "disable-tiling": false, "display-current-layout": { "mod": "mod1", "key": "i" }, "reevaluate-windows": { "mod": "mod1", "key": "z" }, "toggle-focus-follows-mouse": { "mod": "mod2", "key": "x" }, "relaunch-amethyst": { "mod": "mod2", "key": "z" }, "floating": [], "floating-is-blacklist": true, "float-small-windows": true, "small-window-size": 500, "mouse-follows-focus": false, "focus-follows-mouse": false, "enables-layout-hud": true, "enables-layout-hud-on-space-change": true, "enables-window-count-hud": false, "window-margin-size": 0, "window-resize-step": 5, "window-margins": false, "window-minimum-height": 0, "window-minimum-width": 0, "window-max-count": 0, "ignore-menu-bar": false, "use-canary-build": false, "new-windows-to-main": false, "follow-space-thrown-windows": true, "screen-padding-top": 0, "screen-padding-bottom": 0, "screen-padding-left": 0, "screen-padding-right": 0, "debug-layout-info": false, "restore-layouts-on-launch": true } ================================================ FILE: Amethyst/en.lproj/Credits.rtf ================================================ {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} {\colortbl;\red255\green255\blue255;} \paperw9840\paperh8400 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural \f0\b\fs24 \cf0 Engineering: \b0 \ Some people\ \ \b Human Interface Design: \b0 \ Some other people\ \ \b Testing: \b0 \ Hopefully not nobody\ \ \b Documentation: \b0 \ Whoever\ \ \b With special thanks to: \b0 \ Mom\ } ================================================ FILE: Amethyst/en.lproj/InfoPlist.strings ================================================ /* Localized versions of Info.plist keys */ ================================================ FILE: Amethyst/main.swift ================================================ // // main.swift // Amethyst // // Created by Ian Ynda-Hummel on 2/24/20. // Copyright © 2020 Ian Ynda-Hummel. All rights reserved. // import ArgumentParser import Cocoa struct Arguments: ParsableArguments {} struct Amethyst: ParsableCommand { static var configuration: CommandConfiguration = CommandConfiguration( subcommands: [Debug.self, App.self], defaultSubcommand: App.self ) } struct App: ParsableCommand { static var configuration: CommandConfiguration = CommandConfiguration( abstract: "Run the Amethyst application." ) mutating func run() throws { _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) } } if CommandLine.arguments.contains("--debug-info") { print(DebugInfo.description(arguments: CommandLine.arguments)) } else if CommandLine.arguments.dropFirst().first == "test" { _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) } else { do { var command = try Amethyst.parseAsRoot() try command.run() } catch { Arguments.exit(withError: error) } Arguments.exit() } ================================================ FILE: Amethyst.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1A4B46EB20AA7717003D5110 /* NSTableView+Amethyst.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A4B46EA20AA7717003D5110 /* NSTableView+Amethyst.swift */; }; 2A6D9A4125E5D24D006A36B5 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6D9A4025E5D24D006A36B5 /* AppManager.swift */; }; 4000DB10239CA07000365A0C /* WideLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4000DB0F239CA07000365A0C /* WideLayoutTests.swift */; }; 400540F22325CDF4004B8656 /* Reliability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400540F12325CDF4004B8656 /* Reliability.swift */; }; 4006CF881CDFF5F6004CA512 /* NSRunningApplication+Manageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4006CF871CDFF5F6004CA512 /* NSRunningApplication+Manageable.swift */; }; 4006CF8E1CDFFE90004CA512 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4006CF8D1CDFFE90004CA512 /* AppDelegate.swift */; }; 4006CF901CE017BA004CA512 /* UserConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4006CF8F1CE017BA004CA512 /* UserConfiguration.swift */; }; 400A5E9D2327350C00F0A2C3 /* Space.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400A5E9C2327350C00F0A2C3 /* Space.swift */; }; 400D48F42A92AF1E0082750F /* Cartography in Frameworks */ = {isa = PBXBuildFile; productRef = 400D48F32A92AF1E0082750F /* Cartography */; }; 400D48F72A92AF9B0082750F /* LoginServiceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 400D48F62A92AF9B0082750F /* LoginServiceKit */; }; 400D48FA2A92B0130082750F /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 400D48F92A92B0130082750F /* Sparkle */; }; 400D48FD2A92B06B0082750F /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 400D48FC2A92B06B0082750F /* SwiftyJSON */; }; 400D49002A92B0A00082750F /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 400D48FF2A92B0A00082750F /* Yams */; }; 400F2DF32AABF4FC00C1AAE2 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 400F2DF22AABF4FC00C1AAE2 /* KeyboardShortcuts */; }; 40111CC9223370FD003D20BD /* SIWindow+AmethystTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40111CC8223370FD003D20BD /* SIWindow+AmethystTests.swift */; }; 40111CCB22342CC4003D20BD /* DebugPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40111CCA22342CC4003D20BD /* DebugPreferencesViewController.swift */; }; 40111CCD22342CF3003D20BD /* DebugPreferencesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40111CCC22342CF3003D20BD /* DebugPreferencesViewController.xib */; }; 401ADBAF2D55A1B6001FF53A /* recommended-main-pane-ratio.js in Resources */ = {isa = PBXBuildFile; fileRef = 401ADBAE2D55A1B6001FF53A /* recommended-main-pane-ratio.js */; }; 401BBCB62333067F005118F8 /* ColumnLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BBCB52333067F005118F8 /* ColumnLayoutTests.swift */; }; 401BC8981CE7E45300F89B3F /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8971CE7E45300F89B3F /* WindowManager.swift */; }; 401BC89A1CE8C6AE00F89B3F /* HotKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8991CE8C6AE00F89B3F /* HotKeyManager.swift */; }; 401BC89C1CE8D58A00F89B3F /* ShortcutsPreferencesListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC89B1CE8D58A00F89B3F /* ShortcutsPreferencesListItemView.swift */; }; 401BC8A21CE8D86B00F89B3F /* ShortcutsPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8A11CE8D86B00F89B3F /* ShortcutsPreferencesViewController.swift */; }; 401BC8A41CE8DB0800F89B3F /* GeneralPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8A31CE8DB0800F89B3F /* GeneralPreferencesViewController.swift */; }; 401BC8A61CE8E65D00F89B3F /* LayoutNameWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8A51CE8E65D00F89B3F /* LayoutNameWindow.swift */; }; 401BC8A81CE8E94000F89B3F /* WindowsInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8A71CE8E94000F89B3F /* WindowsInformation.swift */; }; 401BC8AE1CE8FD4700F89B3F /* UserConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8AD1CE8FD4700F89B3F /* UserConfigurationTests.swift */; }; 401BC8B41CE9250800F89B3F /* HotKeyRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8B31CE9250800F89B3F /* HotKeyRegistrar.swift */; }; 401BC8B61CE9259000F89B3F /* LayoutType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8B51CE9259000F89B3F /* LayoutType.swift */; }; 401BC8BA1CE9319500F89B3F /* FocusFollowsMouseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401BC8B91CE9319500F89B3F /* FocusFollowsMouseManager.swift */; }; 401C35AF2241BBDD0019ED07 /* ReflowOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C35AE2241BBDD0019ED07 /* ReflowOperation.swift */; }; 401C35B12244626E0019ED07 /* ApplicationObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C35B02244626E0019ED07 /* ApplicationObservation.swift */; }; 401C35B32244650F0019ED07 /* MouseState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C35B22244650F0019ED07 /* MouseState.swift */; }; 401C35B522482EAF0019ED07 /* WindowTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C35B422482EAF0019ED07 /* WindowTransitionCoordinator.swift */; }; 401C35B7224831470019ED07 /* FocusTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C35B6224831470019ED07 /* FocusTransitionCoordinator.swift */; }; 4029C4F11C112478001E4788 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4029C4F01C112478001E4788 /* Layout.swift */; }; 402DA3D72F53DCDB00B08CA2 /* Silica in Frameworks */ = {isa = PBXBuildFile; productRef = 402DA3D62F53DCDB00B08CA2 /* Silica */; }; 402DA3DA2F53DCFE00B08CA2 /* Silica in Frameworks */ = {isa = PBXBuildFile; productRef = 402DA3D92F53DCFE00B08CA2 /* Silica */; }; 402DB6E21742E41A00D1C936 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 402DB6E11742E41A00D1C936 /* Cocoa.framework */; }; 402DB6EC1742E41A00D1C936 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 402DB6EA1742E41A00D1C936 /* InfoPlist.strings */; }; 402DB6F21742E41A00D1C936 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 402DB6F01742E41A00D1C936 /* Credits.rtf */; }; 402DB6F81742E41A00D1C936 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 402DB6F61742E41A00D1C936 /* MainMenu.xib */; }; 402DB6FF1742E44E00D1C936 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 402DB6FE1742E44E00D1C936 /* Carbon.framework */; }; 402F6FA62A81C9E30036B512 /* SkyLight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 402F6FA52A81C9E30036B512 /* SkyLight.framework */; }; 403E1A2A2337173600DB7B2A /* FloatingLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403E1A292337173600DB7B2A /* FloatingLayoutTests.swift */; }; 403E1A2C233719E500DB7B2A /* TallLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403E1A2B233719E500DB7B2A /* TallLayoutTests.swift */; }; 4045416F268FFDA000861BE8 /* CustomLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4045416E268FFDA000861BE8 /* CustomLayout.swift */; }; 404541712697C14A00861BE8 /* CustomLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404541702697C14A00861BE8 /* CustomLayoutTests.swift */; }; 404541742697C22A00861BE8 /* null.js in Resources */ = {isa = PBXBuildFile; fileRef = 404541732697C22A00861BE8 /* null.js */; }; 404541762697C62400861BE8 /* TestBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404541752697C62400861BE8 /* TestBundle.swift */; }; 404541782697CDD000861BE8 /* fullscreen.js in Resources */ = {isa = PBXBuildFile; fileRef = 404541772697CDD000861BE8 /* fullscreen.js */; }; 4045417A2697EBC500861BE8 /* undefined.js in Resources */ = {isa = PBXBuildFile; fileRef = 404541792697EBC500861BE8 /* undefined.js */; }; 4045417C2697EE7800861BE8 /* uniform-columns.js in Resources */ = {isa = PBXBuildFile; fileRef = 4045417B2697EE7800861BE8 /* uniform-columns.js */; }; 4045417E2698030A00861BE8 /* static-ratio-tall.js in Resources */ = {isa = PBXBuildFile; fileRef = 4045417D2698030A00861BE8 /* static-ratio-tall.js */; }; 4046EFCF2236019400113067 /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4046EFCE2236019400113067 /* Window.swift */; }; 4046EFD12238949900113067 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4046EFD02238949900113067 /* Application.swift */; }; 404BE9CE1CFBB6E900D6C537 /* BinarySpacePartitioningLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404BE9CD1CFBB6E900D6C537 /* BinarySpacePartitioningLayout.swift */; }; 404BE9D11CFBDF1900D6C537 /* BinarySpacePartitioningLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404BE9D01CFBDF1900D6C537 /* BinarySpacePartitioningLayoutTests.swift */; }; 40578C19232D9CC0006553A0 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40578C18232D9CC0006553A0 /* Screen.swift */; }; 40578C1B232DC607006553A0 /* TestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40578C1A232DC607006553A0 /* TestScreen.swift */; }; 40583E1329AEF70B008602BB /* ApplicationEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40583E1229AEF70B008602BB /* ApplicationEventHandler.swift */; }; 4058C46F1C4B119500B19D26 /* LayoutNameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4058C46E1C4B119500B19D26 /* LayoutNameWindowController.swift */; }; 4058C47A1C54113D00B19D26 /* GeneralPreferencesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4058C4791C54113D00B19D26 /* GeneralPreferencesViewController.xib */; }; 4058C47E1C54119B00B19D26 /* ShortcutsPreferencesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4058C47D1C54119B00B19D26 /* ShortcutsPreferencesViewController.xib */; }; 4062AD2B1C1F99EA00DB612B /* FloatingLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4062AD2A1C1F99EA00DB612B /* FloatingLayout.swift */; }; 4062AD2D1C1F9B8B00DB612B /* FullscreenLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4062AD2C1C1F9B8B00DB612B /* FullscreenLayout.swift */; }; 4062AD2F1C1F9D6B00DB612B /* TallLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4062AD2E1C1F9D6B00DB612B /* TallLayout.swift */; }; 4062AD311C1FA29600DB612B /* TallRightLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4062AD301C1FA29600DB612B /* TallRightLayout.swift */; }; 4062AD331C1FA48C00DB612B /* ColumnLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4062AD321C1FA48C00DB612B /* ColumnLayout.swift */; }; 4062AD351C1FA62500DB612B /* WideLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4062AD341C1FA62500DB612B /* WideLayout.swift */; }; 4062AD371C1FA83300DB612B /* RowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4062AD361C1FA83300DB612B /* RowLayout.swift */; }; 4062AD3B1C206EA900DB612B /* WidescreenTallLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4062AD3A1C206EA900DB612B /* WidescreenTallLayout.swift */; }; 40637073224EF9B20081299D /* Change.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40637072224EF9B20081299D /* Change.swift */; }; 40637075224EFED70081299D /* CGInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40637074224EFED70081299D /* CGInfo.swift */; }; 40637077224F0EFF0081299D /* Screens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40637076224F0EFF0081299D /* Screens.swift */; }; 4085824C1EA673AE0075A2C3 /* HotKeyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4085824B1EA673AE0075A2C3 /* HotKeyManagerTests.swift */; }; 4087BB042D447F480062A52B /* static-ratio-tall-native-commands.js in Resources */ = {isa = PBXBuildFile; fileRef = 4087BB032D447F480062A52B /* static-ratio-tall-native-commands.js */; }; 409645B31CEE8D950042F459 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409645B21CEE8D950042F459 /* LogManager.swift */; }; 40A7AEA7232ECF3000E79964 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A7AEA6232ECF3000E79964 /* Windows.swift */; }; 40A87FDC2404AEAD005EE9C6 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A87FDB2404AEAD005EE9C6 /* main.swift */; }; 40A87FFD2404B1B4005EE9C6 /* DebugInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A87FFC2404B1B4005EE9C6 /* DebugInfo.swift */; }; 40A87FFF240AF2BC005EE9C6 /* MousePreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A87FFE240AF2BC005EE9C6 /* MousePreferencesViewController.swift */; }; 40A88001240AF2D5005EE9C6 /* MousePreferencesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40A88000240AF2D5005EE9C6 /* MousePreferencesViewController.xib */; }; 40A88003240C40BC005EE9C6 /* LayoutsPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A88002240C40BC005EE9C6 /* LayoutsPreferencesViewController.swift */; }; 40A88005240C40D0005EE9C6 /* LayoutsPreferencesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40A88004240C40D0005EE9C6 /* LayoutsPreferencesViewController.xib */; }; 40AB5BE923AB0A2B00E29346 /* TallRightLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AB5BE823AB0A2B00E29346 /* TallRightLayoutTests.swift */; }; 40AB5BEB23AB182300E29346 /* WidescreenTallLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AB5BEA23AB182300E29346 /* WidescreenTallLayoutTests.swift */; }; 40AB5BED23AC3CF800E29346 /* ThreeColumnLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AB5BEC23AC3CF800E29346 /* ThreeColumnLayoutTests.swift */; }; 40AE15E32A92E9AF00E14536 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = 40AE15E22A92E9AF00E14536 /* SwiftyBeaver */; }; 40AE15E62A92EA7500E14536 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 40AE15E52A92EA7500E14536 /* RxCocoa */; }; 40AE15E82A92EA7500E14536 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 40AE15E72A92EA7500E14536 /* RxSwift */; }; 40AE15EE2A92EBD800E14536 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 40AE15ED2A92EBD800E14536 /* Quick */; }; 40AE15F12A92EC5300E14536 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 40AE15F02A92EC5300E14536 /* Nimble */; }; 40AE16312A943EF900E14536 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 40AE16302A943EF900E14536 /* CoreGraphics.framework */; }; 40AF71DA20BB348300F58EA9 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 40AF71D920BB348300F58EA9 /* Images.xcassets */; }; 40AF71DD20BB401400F58EA9 /* FloatingPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AF71DB20BB401400F58EA9 /* FloatingPreferencesViewController.swift */; }; 40AF71DE20BB401400F58EA9 /* FloatingPreferencesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40AF71DC20BB401400F58EA9 /* FloatingPreferencesViewController.xib */; }; 40B0D145232D2E630021E0A7 /* FullscreenLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B0D144232D2E630021E0A7 /* FullscreenLayoutTests.swift */; }; 40B0D14B232D944C0021E0A7 /* TestWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B0D14A232D944C0021E0A7 /* TestWindow.swift */; }; 40B3927C1814967F009A296B /* LayoutNameWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40B3927B1814967F009A296B /* LayoutNameWindow.xib */; }; 40C1357C1F202AEE00FF9FA7 /* PreferencesWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40C1357B1F202AEE00FF9FA7 /* PreferencesWindow.swift */; }; 40C3F91E1BD1B22E00F58660 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 40C3F91A1BD1B22E00F58660 /* Security.framework */; }; 40C3F91F1BD1B22E00F58660 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 40C3F91B1BD1B22E00F58660 /* SystemConfiguration.framework */; }; 40C3F9231BD1B35E00F58660 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 40C3F9221BD1B35E00F58660 /* libc++.tbd */; }; 40C3F9251BD1B36C00F58660 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 40C3F9241BD1B36C00F58660 /* libz.tbd */; }; 40CEF4301C2B8D21004C3297 /* ScreenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CEF42F1C2B8D21004C3297 /* ScreenManager.swift */; }; 40CF37C029B440A100CDB07A /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 40CF37BF29B440A100CDB07A /* ArgumentParser */; }; 40CF37C229B58C7400CDB07A /* WindowsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CF37C129B58C7400CDB07A /* WindowsInfo.swift */; }; 40CF37C429BAC18300CDB07A /* ScreensInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CF37C329BAC18300CDB07A /* ScreensInfo.swift */; }; 40CF37C629BACD1800CDB07A /* AppsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CF37C529BACD1800CDB07A /* AppsInfo.swift */; }; 40D491D823367590007E0CCB /* RowLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D491D723367590007E0CCB /* RowLayoutTests.swift */; }; 40D491DB23367630007E0CCB /* FrameAssignmentVerification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D491DA23367630007E0CCB /* FrameAssignmentVerification.swift */; }; 40D540F02ABFC7560007F40A /* MASShortcut in Frameworks */ = {isa = PBXBuildFile; productRef = 40D540EF2ABFC7560007F40A /* MASShortcut */; }; 40D82FF129739C5300F3C18B /* extended.js in Resources */ = {isa = PBXBuildFile; fileRef = 40D82FF029739C5300F3C18B /* extended.js */; }; 40DA8B6E27D5AA7300C291AF /* subset.js in Resources */ = {isa = PBXBuildFile; fileRef = 40DA8B6D27D5AA7300C291AF /* subset.js */; }; 40E2D2302D544F1E00D16B87 /* Silica in Frameworks */ = {isa = PBXBuildFile; productRef = 40E2D22F2D544F1E00D16B87 /* Silica */; }; 40E705D8176EAD7800850DA6 /* default.amethyst in Resources */ = {isa = PBXBuildFile; fileRef = 40E705D7176EAD7800850DA6 /* default.amethyst */; }; 40EC47F423F3A30100048B4F /* ScreenManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40EC47F323F3A30100048B4F /* ScreenManagerTests.swift */; }; 40EFFB4A2F0226EF00EDD929 /* Silica in Frameworks */ = {isa = PBXBuildFile; productRef = 40EFFB492F0226EF00EDD929 /* Silica */; }; 4493EAA22139D9F000AA9623 /* ThreeColumnLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4493EAA12139D9EF00AA9623 /* ThreeColumnLayout.swift */; }; AA4AF40D26717DA900D2AE1B /* TwoPaneLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4AF40C26717DA900D2AE1B /* TwoPaneLayout.swift */; }; AAAC6BAC2677DF7B00BEC1B0 /* TwoPaneLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAC6BAB2677DF7B00BEC1B0 /* TwoPaneLayoutTests.swift */; }; F0B42E352A3CA45E00298E30 /* TwoPaneRightLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B42E342A3CA45E00298E30 /* TwoPaneRightLayout.swift */; }; F46629C4272AD7A30040C275 /* FourColumnLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46629C3272AD7A30040C275 /* FourColumnLayout.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 40D95B4C1C6E2ED800AAF433 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 402DB6D61742E41A00D1C936 /* Project object */; proxyType = 1; remoteGlobalIDString = 402DB6DD1742E41A00D1C936; remoteInfo = Amethyst; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 1A4B46EA20AA7717003D5110 /* NSTableView+Amethyst.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Amethyst.swift"; sourceTree = ""; }; 2A6D9A4025E5D24D006A36B5 /* AppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; }; 4000DB0F239CA07000365A0C /* WideLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideLayoutTests.swift; sourceTree = ""; }; 400540F12325CDF4004B8656 /* Reliability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reliability.swift; sourceTree = ""; }; 4006CF871CDFF5F6004CA512 /* NSRunningApplication+Manageable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSRunningApplication+Manageable.swift"; sourceTree = ""; }; 4006CF8D1CDFFE90004CA512 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 4006CF8F1CE017BA004CA512 /* UserConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserConfiguration.swift; sourceTree = ""; }; 400A5E9C2327350C00F0A2C3 /* Space.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Space.swift; sourceTree = ""; }; 40111CC8223370FD003D20BD /* SIWindow+AmethystTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SIWindow+AmethystTests.swift"; sourceTree = ""; }; 40111CCA22342CC4003D20BD /* DebugPreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPreferencesViewController.swift; sourceTree = ""; }; 40111CCC22342CF3003D20BD /* DebugPreferencesViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DebugPreferencesViewController.xib; sourceTree = ""; }; 401A529824D3B63A004359A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 401ADBAE2D55A1B6001FF53A /* recommended-main-pane-ratio.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "recommended-main-pane-ratio.js"; sourceTree = ""; }; 401BBCB52333067F005118F8 /* ColumnLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnLayoutTests.swift; sourceTree = ""; }; 401BC8971CE7E45300F89B3F /* WindowManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 401BC8991CE8C6AE00F89B3F /* HotKeyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HotKeyManager.swift; path = ../Events/HotKeyManager.swift; sourceTree = ""; }; 401BC89B1CE8D58A00F89B3F /* ShortcutsPreferencesListItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutsPreferencesListItemView.swift; sourceTree = ""; }; 401BC8A11CE8D86B00F89B3F /* ShortcutsPreferencesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutsPreferencesViewController.swift; sourceTree = ""; }; 401BC8A31CE8DB0800F89B3F /* GeneralPreferencesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralPreferencesViewController.swift; sourceTree = ""; }; 401BC8A51CE8E65D00F89B3F /* LayoutNameWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutNameWindow.swift; sourceTree = ""; }; 401BC8A71CE8E94000F89B3F /* WindowsInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowsInformation.swift; sourceTree = ""; }; 401BC8A91CE8F49200F89B3F /* Amethyst-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Amethyst-Bridging-Header.h"; sourceTree = ""; }; 401BC8AC1CE8FD4600F89B3F /* AmethystTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AmethystTests-Bridging-Header.h"; sourceTree = ""; }; 401BC8AD1CE8FD4700F89B3F /* UserConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserConfigurationTests.swift; sourceTree = ""; }; 401BC8B31CE9250800F89B3F /* HotKeyRegistrar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKeyRegistrar.swift; sourceTree = ""; }; 401BC8B51CE9259000F89B3F /* LayoutType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutType.swift; sourceTree = ""; }; 401BC8B91CE9319500F89B3F /* FocusFollowsMouseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusFollowsMouseManager.swift; sourceTree = ""; }; 401C35AE2241BBDD0019ED07 /* ReflowOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReflowOperation.swift; sourceTree = ""; }; 401C35B02244626E0019ED07 /* ApplicationObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationObservation.swift; sourceTree = ""; }; 401C35B22244650F0019ED07 /* MouseState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseState.swift; sourceTree = ""; }; 401C35B422482EAF0019ED07 /* WindowTransitionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTransitionCoordinator.swift; sourceTree = ""; }; 401C35B6224831470019ED07 /* FocusTransitionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusTransitionCoordinator.swift; sourceTree = ""; }; 4029C4F01C112478001E4788 /* Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; 402DB6DE1742E41A00D1C936 /* Amethyst.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Amethyst.app; sourceTree = BUILT_PRODUCTS_DIR; }; 402DB6E11742E41A00D1C936 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 402DB6E41742E41A00D1C936 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 402DB6E61742E41A00D1C936 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 402DB6E91742E41A00D1C936 /* Amethyst-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Amethyst-Info.plist"; sourceTree = ""; }; 402DB6EB1742E41A00D1C936 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 402DB6F11742E41A00D1C936 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 402DB6FE1742E44E00D1C936 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; 402F6FA52A81C9E30036B512 /* SkyLight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SkyLight.framework; path = /System/Library/PrivateFrameworks/SkyLight.framework; sourceTree = ""; }; 40378E22238F39B900D11E22 /* Amethyst.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Amethyst.entitlements; sourceTree = ""; }; 403E1A292337173600DB7B2A /* FloatingLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingLayoutTests.swift; sourceTree = ""; }; 403E1A2B233719E500DB7B2A /* TallLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TallLayoutTests.swift; sourceTree = ""; }; 4045416E268FFDA000861BE8 /* CustomLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayout.swift; sourceTree = ""; }; 404541702697C14A00861BE8 /* CustomLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayoutTests.swift; sourceTree = ""; }; 404541732697C22A00861BE8 /* null.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = null.js; sourceTree = ""; }; 404541752697C62400861BE8 /* TestBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundle.swift; sourceTree = ""; }; 404541772697CDD000861BE8 /* fullscreen.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = fullscreen.js; sourceTree = ""; }; 404541792697EBC500861BE8 /* undefined.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = undefined.js; sourceTree = ""; }; 4045417B2697EE7800861BE8 /* uniform-columns.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "uniform-columns.js"; sourceTree = ""; }; 4045417D2698030A00861BE8 /* static-ratio-tall.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "static-ratio-tall.js"; sourceTree = ""; }; 4046EFCE2236019400113067 /* Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = ""; }; 4046EFD02238949900113067 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 404BE9CD1CFBB6E900D6C537 /* BinarySpacePartitioningLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinarySpacePartitioningLayout.swift; sourceTree = ""; }; 404BE9D01CFBDF1900D6C537 /* BinarySpacePartitioningLayoutTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinarySpacePartitioningLayoutTests.swift; sourceTree = ""; }; 40578C18232D9CC0006553A0 /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; 40578C1A232DC607006553A0 /* TestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestScreen.swift; sourceTree = ""; }; 40583E1229AEF70B008602BB /* ApplicationEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationEventHandler.swift; sourceTree = ""; }; 4058C46E1C4B119500B19D26 /* LayoutNameWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutNameWindowController.swift; sourceTree = ""; }; 4058C4791C54113D00B19D26 /* GeneralPreferencesViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = GeneralPreferencesViewController.xib; sourceTree = ""; }; 4058C47D1C54119B00B19D26 /* ShortcutsPreferencesViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShortcutsPreferencesViewController.xib; sourceTree = ""; }; 4062AD2A1C1F99EA00DB612B /* FloatingLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingLayout.swift; sourceTree = ""; }; 4062AD2C1C1F9B8B00DB612B /* FullscreenLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullscreenLayout.swift; sourceTree = ""; }; 4062AD2E1C1F9D6B00DB612B /* TallLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TallLayout.swift; sourceTree = ""; }; 4062AD301C1FA29600DB612B /* TallRightLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TallRightLayout.swift; sourceTree = ""; }; 4062AD321C1FA48C00DB612B /* ColumnLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColumnLayout.swift; sourceTree = ""; }; 4062AD341C1FA62500DB612B /* WideLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WideLayout.swift; sourceTree = ""; }; 4062AD361C1FA83300DB612B /* RowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowLayout.swift; sourceTree = ""; }; 4062AD3A1C206EA900DB612B /* WidescreenTallLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidescreenTallLayout.swift; sourceTree = ""; }; 40637072224EF9B20081299D /* Change.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Change.swift; sourceTree = ""; }; 40637074224EFED70081299D /* CGInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGInfo.swift; sourceTree = ""; }; 40637076224F0EFF0081299D /* Screens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screens.swift; sourceTree = ""; }; 4076368C2394BFB500308336 /* AmethystDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AmethystDebug.entitlements; sourceTree = ""; }; 4085824B1EA673AE0075A2C3 /* HotKeyManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKeyManagerTests.swift; sourceTree = ""; }; 4087BB032D447F480062A52B /* static-ratio-tall-native-commands.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "static-ratio-tall-native-commands.js"; sourceTree = ""; }; 409645B21CEE8D950042F459 /* LogManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; 40A7AEA6232ECF3000E79964 /* Windows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = ""; }; 40A87FDB2404AEAD005EE9C6 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 40A87FFC2404B1B4005EE9C6 /* DebugInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugInfo.swift; sourceTree = ""; }; 40A87FFE240AF2BC005EE9C6 /* MousePreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MousePreferencesViewController.swift; sourceTree = ""; }; 40A88000240AF2D5005EE9C6 /* MousePreferencesViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MousePreferencesViewController.xib; sourceTree = ""; }; 40A88002240C40BC005EE9C6 /* LayoutsPreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutsPreferencesViewController.swift; sourceTree = ""; }; 40A88004240C40D0005EE9C6 /* LayoutsPreferencesViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LayoutsPreferencesViewController.xib; sourceTree = ""; }; 40AB5BE823AB0A2B00E29346 /* TallRightLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TallRightLayoutTests.swift; sourceTree = ""; }; 40AB5BEA23AB182300E29346 /* WidescreenTallLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidescreenTallLayoutTests.swift; sourceTree = ""; }; 40AB5BEC23AC3CF800E29346 /* ThreeColumnLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreeColumnLayoutTests.swift; sourceTree = ""; }; 40AE16302A943EF900E14536 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 40AF71D920BB348300F58EA9 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 40AF71DB20BB401400F58EA9 /* FloatingPreferencesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingPreferencesViewController.swift; sourceTree = ""; }; 40AF71DC20BB401400F58EA9 /* FloatingPreferencesViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FloatingPreferencesViewController.xib; sourceTree = ""; }; 40B0D144232D2E630021E0A7 /* FullscreenLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenLayoutTests.swift; sourceTree = ""; }; 40B0D14A232D944C0021E0A7 /* TestWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWindow.swift; sourceTree = ""; }; 40B3927B1814967F009A296B /* LayoutNameWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LayoutNameWindow.xib; sourceTree = ""; }; 40C1357B1F202AEE00FF9FA7 /* PreferencesWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindow.swift; sourceTree = ""; }; 40C3F91A1BD1B22E00F58660 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 40C3F91B1BD1B22E00F58660 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 40C3F9221BD1B35E00F58660 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; 40C3F9241BD1B36C00F58660 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 40CEF42F1C2B8D21004C3297 /* ScreenManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = ""; usesTabs = 0; }; 40CF37C129B58C7400CDB07A /* WindowsInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsInfo.swift; sourceTree = ""; }; 40CF37C329BAC18300CDB07A /* ScreensInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreensInfo.swift; sourceTree = ""; }; 40CF37C529BACD1800CDB07A /* AppsInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsInfo.swift; sourceTree = ""; }; 40D491D723367590007E0CCB /* RowLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowLayoutTests.swift; sourceTree = ""; }; 40D491DA23367630007E0CCB /* FrameAssignmentVerification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameAssignmentVerification.swift; sourceTree = ""; }; 40D82FF029739C5300F3C18B /* extended.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = extended.js; sourceTree = ""; }; 40D95B471C6E2ED800AAF433 /* AmethystTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AmethystTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 40D95B4B1C6E2ED800AAF433 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40DA8B6D27D5AA7300C291AF /* subset.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = subset.js; sourceTree = ""; }; 40E705D7176EAD7800850DA6 /* default.amethyst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = default.amethyst; sourceTree = ""; }; 40EC47F323F3A30100048B4F /* ScreenManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManagerTests.swift; sourceTree = ""; }; 4493EAA12139D9EF00AA9623 /* ThreeColumnLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeColumnLayout.swift; sourceTree = ""; }; AA4AF40C26717DA900D2AE1B /* TwoPaneLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoPaneLayout.swift; sourceTree = ""; }; AAAC6BAB2677DF7B00BEC1B0 /* TwoPaneLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoPaneLayoutTests.swift; sourceTree = ""; }; F0B42E342A3CA45E00298E30 /* TwoPaneRightLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoPaneRightLayout.swift; sourceTree = ""; }; F46629C3272AD7A30040C275 /* FourColumnLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FourColumnLayout.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 402DB6DB1742E41A00D1C936 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 40D540F02ABFC7560007F40A /* MASShortcut in Frameworks */, 400D48FD2A92B06B0082750F /* SwiftyJSON in Frameworks */, 40C3F9251BD1B36C00F58660 /* libz.tbd in Frameworks */, 40AE16312A943EF900E14536 /* CoreGraphics.framework in Frameworks */, 40AE15E32A92E9AF00E14536 /* SwiftyBeaver in Frameworks */, 40C3F9231BD1B35E00F58660 /* libc++.tbd in Frameworks */, 402DA3DA2F53DCFE00B08CA2 /* Silica in Frameworks */, 402DB6FF1742E44E00D1C936 /* Carbon.framework in Frameworks */, 402DA3D72F53DCDB00B08CA2 /* Silica in Frameworks */, 400D49002A92B0A00082750F /* Yams in Frameworks */, 40AE15E62A92EA7500E14536 /* RxCocoa in Frameworks */, 40C3F91F1BD1B22E00F58660 /* SystemConfiguration.framework in Frameworks */, 40AE15E82A92EA7500E14536 /* RxSwift in Frameworks */, 400D48F72A92AF9B0082750F /* LoginServiceKit in Frameworks */, 402DB6E21742E41A00D1C936 /* Cocoa.framework in Frameworks */, 400D48F42A92AF1E0082750F /* Cartography in Frameworks */, 40C3F91E1BD1B22E00F58660 /* Security.framework in Frameworks */, 40E2D2302D544F1E00D16B87 /* Silica in Frameworks */, 40CF37C029B440A100CDB07A /* ArgumentParser in Frameworks */, 400D48FA2A92B0130082750F /* Sparkle in Frameworks */, 400F2DF32AABF4FC00C1AAE2 /* KeyboardShortcuts in Frameworks */, 40EFFB4A2F0226EF00EDD929 /* Silica in Frameworks */, 402F6FA62A81C9E30036B512 /* SkyLight.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 40D95B441C6E2ED800AAF433 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 40AE15F12A92EC5300E14536 /* Nimble in Frameworks */, 40AE15EE2A92EBD800E14536 /* Quick in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4008E724190C4D9D0049E2F6 /* Preferences */ = { isa = PBXGroup; children = ( 40111CCA22342CC4003D20BD /* DebugPreferencesViewController.swift */, 40111CCC22342CF3003D20BD /* DebugPreferencesViewController.xib */, 40AF71DB20BB401400F58EA9 /* FloatingPreferencesViewController.swift */, 40AF71DC20BB401400F58EA9 /* FloatingPreferencesViewController.xib */, 401BC8A31CE8DB0800F89B3F /* GeneralPreferencesViewController.swift */, 4058C4791C54113D00B19D26 /* GeneralPreferencesViewController.xib */, 40A88002240C40BC005EE9C6 /* LayoutsPreferencesViewController.swift */, 40A88004240C40D0005EE9C6 /* LayoutsPreferencesViewController.xib */, 40A87FFE240AF2BC005EE9C6 /* MousePreferencesViewController.swift */, 40A88000240AF2D5005EE9C6 /* MousePreferencesViewController.xib */, 401BC89B1CE8D58A00F89B3F /* ShortcutsPreferencesListItemView.swift */, 401BC8A11CE8D86B00F89B3F /* ShortcutsPreferencesViewController.swift */, 4058C47D1C54119B00B19D26 /* ShortcutsPreferencesViewController.xib */, 4006CF8F1CE017BA004CA512 /* UserConfiguration.swift */, ); path = Preferences; sourceTree = ""; }; 40111CC7223370E1003D20BD /* Categories */ = { isa = PBXGroup; children = ( 40111CC8223370FD003D20BD /* SIWindow+AmethystTests.swift */, ); path = Categories; sourceTree = ""; }; 401BC8AA1CE8F82700F89B3F /* Managers */ = { isa = PBXGroup; children = ( 2A6D9A4025E5D24D006A36B5 /* AppManager.swift */, 401BC8B91CE9319500F89B3F /* FocusFollowsMouseManager.swift */, 401C35B6224831470019ED07 /* FocusTransitionCoordinator.swift */, 401BC8991CE8C6AE00F89B3F /* HotKeyManager.swift */, 401BC8B31CE9250800F89B3F /* HotKeyRegistrar.swift */, 401BC8B51CE9259000F89B3F /* LayoutType.swift */, 409645B21CEE8D950042F459 /* LogManager.swift */, 40CEF42F1C2B8D21004C3297 /* ScreenManager.swift */, 40637076224F0EFF0081299D /* Screens.swift */, 401BC8971CE7E45300F89B3F /* WindowManager.swift */, 40A7AEA6232ECF3000E79964 /* Windows.swift */, 401C35B422482EAF0019ED07 /* WindowTransitionCoordinator.swift */, ); path = Managers; sourceTree = ""; }; 401BC8AB1CE8F84500F89B3F /* Preferences */ = { isa = PBXGroup; children = ( 401BC8AD1CE8FD4700F89B3F /* UserConfigurationTests.swift */, ); name = Preferences; path = Configuration; sourceTree = ""; }; 401C35AD2241BA760019ED07 /* Layouts */ = { isa = PBXGroup; children = ( 404BE9CD1CFBB6E900D6C537 /* BinarySpacePartitioningLayout.swift */, 4062AD321C1FA48C00DB612B /* ColumnLayout.swift */, 4062AD2A1C1F99EA00DB612B /* FloatingLayout.swift */, 4062AD2C1C1F9B8B00DB612B /* FullscreenLayout.swift */, 4062AD361C1FA83300DB612B /* RowLayout.swift */, 4062AD2E1C1F9D6B00DB612B /* TallLayout.swift */, AA4AF40C26717DA900D2AE1B /* TwoPaneLayout.swift */, 4062AD301C1FA29600DB612B /* TallRightLayout.swift */, 4493EAA12139D9EF00AA9623 /* ThreeColumnLayout.swift */, F46629C3272AD7A30040C275 /* FourColumnLayout.swift */, 4062AD341C1FA62500DB612B /* WideLayout.swift */, 4062AD3A1C206EA900DB612B /* WidescreenTallLayout.swift */, 4045416E268FFDA000861BE8 /* CustomLayout.swift */, F0B42E342A3CA45E00298E30 /* TwoPaneRightLayout.swift */, ); path = Layouts; sourceTree = ""; }; 402DB6D51742E41A00D1C936 = { isa = PBXGroup; children = ( 402DB6E71742E41A00D1C936 /* Amethyst */, 40D95B481C6E2ED800AAF433 /* AmethystTests */, 402DB6E01742E41A00D1C936 /* Frameworks */, 402DB6DF1742E41A00D1C936 /* Products */, ); sourceTree = ""; usesTabs = 0; }; 402DB6DF1742E41A00D1C936 /* Products */ = { isa = PBXGroup; children = ( 402DB6DE1742E41A00D1C936 /* Amethyst.app */, 40D95B471C6E2ED800AAF433 /* AmethystTests.xctest */, ); name = Products; sourceTree = ""; }; 402DB6E01742E41A00D1C936 /* Frameworks */ = { isa = PBXGroup; children = ( 40AE16302A943EF900E14536 /* CoreGraphics.framework */, 402F6FA52A81C9E30036B512 /* SkyLight.framework */, 40C3F9241BD1B36C00F58660 /* libz.tbd */, 40C3F9221BD1B35E00F58660 /* libc++.tbd */, 40C3F91A1BD1B22E00F58660 /* Security.framework */, 40C3F91B1BD1B22E00F58660 /* SystemConfiguration.framework */, 402DB6FE1742E44E00D1C936 /* Carbon.framework */, 402DB6E11742E41A00D1C936 /* Cocoa.framework */, 402DB6E31742E41A00D1C936 /* Other Frameworks */, ); name = Frameworks; sourceTree = ""; }; 402DB6E31742E41A00D1C936 /* Other Frameworks */ = { isa = PBXGroup; children = ( 402DB6E41742E41A00D1C936 /* AppKit.framework */, 402DB6E61742E41A00D1C936 /* Foundation.framework */, ); name = "Other Frameworks"; sourceTree = ""; }; 402DB6E71742E41A00D1C936 /* Amethyst */ = { isa = PBXGroup; children = ( 4076368C2394BFB500308336 /* AmethystDebug.entitlements */, 40378E22238F39B900D11E22 /* Amethyst.entitlements */, 40A87FFB2404B194005EE9C6 /* Debug */, 4046EFCD2236018400113067 /* Model */, 401BC8AA1CE8F82700F89B3F /* Managers */, 4008E724190C4D9D0049E2F6 /* Preferences */, 40B392831814977D009A296B /* View */, 40B337E51745D1600001D8FC /* Categories */, 40B337E11745CE340001D8FC /* Layout */, 4006CF8D1CDFFE90004CA512 /* AppDelegate.swift */, 402DB6F61742E41A00D1C936 /* MainMenu.xib */, 40AF71D920BB348300F58EA9 /* Images.xcassets */, 402DB6E81742E41A00D1C936 /* Supporting Files */, ); path = Amethyst; sourceTree = ""; }; 402DB6E81742E41A00D1C936 /* Supporting Files */ = { isa = PBXGroup; children = ( 401BC8A91CE8F49200F89B3F /* Amethyst-Bridging-Header.h */, 402DB6E91742E41A00D1C936 /* Amethyst-Info.plist */, 402DB6EA1742E41A00D1C936 /* InfoPlist.strings */, 402DB6F01742E41A00D1C936 /* Credits.rtf */, 40E705D7176EAD7800850DA6 /* default.amethyst */, 40A87FDB2404AEAD005EE9C6 /* main.swift */, ); name = "Supporting Files"; sourceTree = ""; }; 404541722697C16B00861BE8 /* CustomLayouts */ = { isa = PBXGroup; children = ( 40D82FF029739C5300F3C18B /* extended.js */, 404541772697CDD000861BE8 /* fullscreen.js */, 404541732697C22A00861BE8 /* null.js */, 401ADBAE2D55A1B6001FF53A /* recommended-main-pane-ratio.js */, 4045417D2698030A00861BE8 /* static-ratio-tall.js */, 4087BB032D447F480062A52B /* static-ratio-tall-native-commands.js */, 40DA8B6D27D5AA7300C291AF /* subset.js */, 404541792697EBC500861BE8 /* undefined.js */, 4045417B2697EE7800861BE8 /* uniform-columns.js */, ); path = CustomLayouts; sourceTree = ""; }; 4046EFCD2236018400113067 /* Model */ = { isa = PBXGroup; children = ( 4046EFD02238949900113067 /* Application.swift */, 401C35B02244626E0019ED07 /* ApplicationObservation.swift */, 40637074224EFED70081299D /* CGInfo.swift */, 40637072224EF9B20081299D /* Change.swift */, 401C35B22244650F0019ED07 /* MouseState.swift */, 400540F12325CDF4004B8656 /* Reliability.swift */, 40578C18232D9CC0006553A0 /* Screen.swift */, 400A5E9C2327350C00F0A2C3 /* Space.swift */, 4046EFCE2236019400113067 /* Window.swift */, 401BC8A71CE8E94000F89B3F /* WindowsInformation.swift */, 40583E1229AEF70B008602BB /* ApplicationEventHandler.swift */, ); path = Model; sourceTree = ""; }; 404BE9CF1CFBDEFD00D6C537 /* Layout */ = { isa = PBXGroup; children = ( 404BE9D01CFBDF1900D6C537 /* BinarySpacePartitioningLayoutTests.swift */, 401BBCB52333067F005118F8 /* ColumnLayoutTests.swift */, 404541702697C14A00861BE8 /* CustomLayoutTests.swift */, 403E1A292337173600DB7B2A /* FloatingLayoutTests.swift */, 40B0D144232D2E630021E0A7 /* FullscreenLayoutTests.swift */, 40D491D723367590007E0CCB /* RowLayoutTests.swift */, 403E1A2B233719E500DB7B2A /* TallLayoutTests.swift */, 40AB5BE823AB0A2B00E29346 /* TallRightLayoutTests.swift */, 40AB5BEC23AC3CF800E29346 /* ThreeColumnLayoutTests.swift */, AAAC6BAB2677DF7B00BEC1B0 /* TwoPaneLayoutTests.swift */, 4000DB0F239CA07000365A0C /* WideLayoutTests.swift */, 40AB5BEA23AB182300E29346 /* WidescreenTallLayoutTests.swift */, ); path = Layout; sourceTree = ""; }; 4085824A1EA673950075A2C3 /* Managers */ = { isa = PBXGroup; children = ( 4085824B1EA673AE0075A2C3 /* HotKeyManagerTests.swift */, 40EC47F323F3A30100048B4F /* ScreenManagerTests.swift */, ); path = Managers; sourceTree = ""; }; 40A87FFB2404B194005EE9C6 /* Debug */ = { isa = PBXGroup; children = ( 40A87FFC2404B1B4005EE9C6 /* DebugInfo.swift */, 40CF37C129B58C7400CDB07A /* WindowsInfo.swift */, 40CF37C329BAC18300CDB07A /* ScreensInfo.swift */, 40CF37C529BACD1800CDB07A /* AppsInfo.swift */, ); path = Debug; sourceTree = ""; }; 40B0D148232D942B0021E0A7 /* Tests */ = { isa = PBXGroup; children = ( 4085824A1EA673950075A2C3 /* Managers */, 404BE9CF1CFBDEFD00D6C537 /* Layout */, 401BC8AB1CE8F84500F89B3F /* Preferences */, 40111CC7223370E1003D20BD /* Categories */, ); path = Tests; sourceTree = ""; }; 40B0D149232D94410021E0A7 /* Model */ = { isa = PBXGroup; children = ( 404541722697C16B00861BE8 /* CustomLayouts */, 40578C1A232DC607006553A0 /* TestScreen.swift */, 40B0D14A232D944C0021E0A7 /* TestWindow.swift */, ); path = Model; sourceTree = ""; }; 40B337E11745CE340001D8FC /* Layout */ = { isa = PBXGroup; children = ( 401C35AD2241BA760019ED07 /* Layouts */, 4029C4F01C112478001E4788 /* Layout.swift */, 401C35AE2241BBDD0019ED07 /* ReflowOperation.swift */, ); path = Layout; sourceTree = ""; }; 40B337E51745D1600001D8FC /* Categories */ = { isa = PBXGroup; children = ( 4006CF871CDFF5F6004CA512 /* NSRunningApplication+Manageable.swift */, 1A4B46EA20AA7717003D5110 /* NSTableView+Amethyst.swift */, ); path = Categories; sourceTree = ""; }; 40B392831814977D009A296B /* View */ = { isa = PBXGroup; children = ( 40B3927B1814967F009A296B /* LayoutNameWindow.xib */, 401BC8A51CE8E65D00F89B3F /* LayoutNameWindow.swift */, 4058C46E1C4B119500B19D26 /* LayoutNameWindowController.swift */, 40C1357B1F202AEE00FF9FA7 /* PreferencesWindow.swift */, ); path = View; sourceTree = ""; }; 40D491D9233675E3007E0CCB /* Helpers */ = { isa = PBXGroup; children = ( 40D491DA23367630007E0CCB /* FrameAssignmentVerification.swift */, 404541752697C62400861BE8 /* TestBundle.swift */, ); path = Helpers; sourceTree = ""; }; 40D95B481C6E2ED800AAF433 /* AmethystTests */ = { isa = PBXGroup; children = ( 40D491D9233675E3007E0CCB /* Helpers */, 40B0D149232D94410021E0A7 /* Model */, 40B0D148232D942B0021E0A7 /* Tests */, 40D95B4B1C6E2ED800AAF433 /* Info.plist */, 401BC8AC1CE8FD4600F89B3F /* AmethystTests-Bridging-Header.h */, ); path = AmethystTests; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 402DB6DD1742E41A00D1C936 /* Amethyst */ = { isa = PBXNativeTarget; buildConfigurationList = 402DB6FB1742E41A00D1C936 /* Build configuration list for PBXNativeTarget "Amethyst" */; buildPhases = ( 4058C4701C4C4F9E00B19D26 /* Lint */, 402DB6DA1742E41A00D1C936 /* Sources */, 402DB6DB1742E41A00D1C936 /* Frameworks */, 402DB6DC1742E41A00D1C936 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Amethyst; packageProductDependencies = ( 40CF37BF29B440A100CDB07A /* ArgumentParser */, 400D48F32A92AF1E0082750F /* Cartography */, 400D48F62A92AF9B0082750F /* LoginServiceKit */, 400D48F92A92B0130082750F /* Sparkle */, 400D48FC2A92B06B0082750F /* SwiftyJSON */, 400D48FF2A92B0A00082750F /* Yams */, 40AE15E22A92E9AF00E14536 /* SwiftyBeaver */, 40AE15E52A92EA7500E14536 /* RxCocoa */, 40AE15E72A92EA7500E14536 /* RxSwift */, 400F2DF22AABF4FC00C1AAE2 /* KeyboardShortcuts */, 40D540EF2ABFC7560007F40A /* MASShortcut */, 40E2D22F2D544F1E00D16B87 /* Silica */, 40EFFB492F0226EF00EDD929 /* Silica */, 402DA3D62F53DCDB00B08CA2 /* Silica */, 402DA3D92F53DCFE00B08CA2 /* Silica */, ); productName = Amethyst; productReference = 402DB6DE1742E41A00D1C936 /* Amethyst.app */; productType = "com.apple.product-type.application"; }; 40D95B461C6E2ED800AAF433 /* AmethystTests */ = { isa = PBXNativeTarget; buildConfigurationList = 40D95B4E1C6E2ED800AAF433 /* Build configuration list for PBXNativeTarget "AmethystTests" */; buildPhases = ( 40D95B431C6E2ED800AAF433 /* Sources */, 40D95B441C6E2ED800AAF433 /* Frameworks */, 40D95B451C6E2ED800AAF433 /* Resources */, ); buildRules = ( ); dependencies = ( 40D95B4D1C6E2ED800AAF433 /* PBXTargetDependency */, ); name = AmethystTests; packageProductDependencies = ( 40AE15ED2A92EBD800E14536 /* Quick */, 40AE15F02A92EC5300E14536 /* Nimble */, ); productName = AmethystTests; productReference = 40D95B471C6E2ED800AAF433 /* AmethystTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 402DB6D61742E41A00D1C936 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; CLASSPREFIX = AM; LastSwiftUpdateCheck = 0730; LastTestingUpgradeCheck = 0700; LastUpgradeCheck = 1500; ORGANIZATIONNAME = "Ian Ynda-Hummel"; TargetAttributes = { 402DB6DD1742E41A00D1C936 = { LastSwiftMigration = 1010; ProvisioningStyle = Automatic; }; 40D95B461C6E2ED800AAF433 = { CreatedOnToolsVersion = 7.2.1; LastSwiftMigration = 1010; ProvisioningStyle = Automatic; TestTargetID = 402DB6DD1742E41A00D1C936; }; }; }; buildConfigurationList = 402DB6D91742E41A00D1C936 /* Build configuration list for PBXProject "Amethyst" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 402DB6D51742E41A00D1C936; packageReferences = ( 40CF37BE29B440A100CDB07A /* XCRemoteSwiftPackageReference "swift-argument-parser" */, 400D48F22A92AF1E0082750F /* XCRemoteSwiftPackageReference "Cartography" */, 400D48F52A92AF9B0082750F /* XCRemoteSwiftPackageReference "LoginServiceKit" */, 400D48F82A92B0130082750F /* XCRemoteSwiftPackageReference "Sparkle" */, 400D48FB2A92B06A0082750F /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 400D48FE2A92B0A00082750F /* XCRemoteSwiftPackageReference "Yams" */, 40AE15E12A92E9AF00E14536 /* XCRemoteSwiftPackageReference "SwiftyBeaver" */, 40AE15E42A92EA7400E14536 /* XCRemoteSwiftPackageReference "RxSwift" */, 40AE15EC2A92EBD800E14536 /* XCRemoteSwiftPackageReference "Quick" */, 40AE15EF2A92EC5300E14536 /* XCRemoteSwiftPackageReference "Nimble" */, 400F2DF12AABF4FC00C1AAE2 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 40D540EE2ABFC7560007F40A /* XCRemoteSwiftPackageReference "MASShortcut" */, 402DA3D82F53DCFE00B08CA2 /* XCRemoteSwiftPackageReference "silica" */, ); productRefGroup = 402DB6DF1742E41A00D1C936 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 402DB6DD1742E41A00D1C936 /* Amethyst */, 40D95B461C6E2ED800AAF433 /* AmethystTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 402DB6DC1742E41A00D1C936 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4058C47A1C54113D00B19D26 /* GeneralPreferencesViewController.xib in Resources */, 40111CCD22342CF3003D20BD /* DebugPreferencesViewController.xib in Resources */, 40A88001240AF2D5005EE9C6 /* MousePreferencesViewController.xib in Resources */, 402DB6EC1742E41A00D1C936 /* InfoPlist.strings in Resources */, 402DB6F21742E41A00D1C936 /* Credits.rtf in Resources */, 40AF71DA20BB348300F58EA9 /* Images.xcassets in Resources */, 402DB6F81742E41A00D1C936 /* MainMenu.xib in Resources */, 4058C47E1C54119B00B19D26 /* ShortcutsPreferencesViewController.xib in Resources */, 40B3927C1814967F009A296B /* LayoutNameWindow.xib in Resources */, 40AF71DE20BB401400F58EA9 /* FloatingPreferencesViewController.xib in Resources */, 40E705D8176EAD7800850DA6 /* default.amethyst in Resources */, 40A88005240C40D0005EE9C6 /* LayoutsPreferencesViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 40D95B451C6E2ED800AAF433 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 4087BB042D447F480062A52B /* static-ratio-tall-native-commands.js in Resources */, 4045417E2698030A00861BE8 /* static-ratio-tall.js in Resources */, 404541742697C22A00861BE8 /* null.js in Resources */, 4045417A2697EBC500861BE8 /* undefined.js in Resources */, 4045417C2697EE7800861BE8 /* uniform-columns.js in Resources */, 40DA8B6E27D5AA7300C291AF /* subset.js in Resources */, 40D82FF129739C5300F3C18B /* extended.js in Resources */, 401ADBAF2D55A1B6001FF53A /* recommended-main-pane-ratio.js in Resources */, 404541782697CDD000861BE8 /* fullscreen.js in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 4058C4701C4C4F9E00B19D26 /* Lint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = Lint; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nswiftlint --fix\nswiftlint\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 402DB6DA1742E41A00D1C936 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 40CF37C229B58C7400CDB07A /* WindowsInfo.swift in Sources */, 40AF71DD20BB401400F58EA9 /* FloatingPreferencesViewController.swift in Sources */, 40A88003240C40BC005EE9C6 /* LayoutsPreferencesViewController.swift in Sources */, 4046EFD12238949900113067 /* Application.swift in Sources */, 4062AD331C1FA48C00DB612B /* ColumnLayout.swift in Sources */, 409645B31CEE8D950042F459 /* LogManager.swift in Sources */, 4006CF881CDFF5F6004CA512 /* NSRunningApplication+Manageable.swift in Sources */, 401BC89A1CE8C6AE00F89B3F /* HotKeyManager.swift in Sources */, 4062AD2F1C1F9D6B00DB612B /* TallLayout.swift in Sources */, 40578C19232D9CC0006553A0 /* Screen.swift in Sources */, 401BC89C1CE8D58A00F89B3F /* ShortcutsPreferencesListItemView.swift in Sources */, 400540F22325CDF4004B8656 /* Reliability.swift in Sources */, 4029C4F11C112478001E4788 /* Layout.swift in Sources */, AA4AF40D26717DA900D2AE1B /* TwoPaneLayout.swift in Sources */, 401BC8A41CE8DB0800F89B3F /* GeneralPreferencesViewController.swift in Sources */, 2A6D9A4125E5D24D006A36B5 /* AppManager.swift in Sources */, 4046EFCF2236019400113067 /* Window.swift in Sources */, 40A87FDC2404AEAD005EE9C6 /* main.swift in Sources */, F46629C4272AD7A30040C275 /* FourColumnLayout.swift in Sources */, 401BC8A61CE8E65D00F89B3F /* LayoutNameWindow.swift in Sources */, 401C35AF2241BBDD0019ED07 /* ReflowOperation.swift in Sources */, 401BC8B41CE9250800F89B3F /* HotKeyRegistrar.swift in Sources */, 40637073224EF9B20081299D /* Change.swift in Sources */, 40CEF4301C2B8D21004C3297 /* ScreenManager.swift in Sources */, 401C35B522482EAF0019ED07 /* WindowTransitionCoordinator.swift in Sources */, 40CF37C629BACD1800CDB07A /* AppsInfo.swift in Sources */, 40A87FFD2404B1B4005EE9C6 /* DebugInfo.swift in Sources */, 4493EAA22139D9F000AA9623 /* ThreeColumnLayout.swift in Sources */, 40A7AEA7232ECF3000E79964 /* Windows.swift in Sources */, F0B42E352A3CA45E00298E30 /* TwoPaneRightLayout.swift in Sources */, 404BE9CE1CFBB6E900D6C537 /* BinarySpacePartitioningLayout.swift in Sources */, 4062AD3B1C206EA900DB612B /* WidescreenTallLayout.swift in Sources */, 4058C46F1C4B119500B19D26 /* LayoutNameWindowController.swift in Sources */, 4062AD311C1FA29600DB612B /* TallRightLayout.swift in Sources */, 401BC8B61CE9259000F89B3F /* LayoutType.swift in Sources */, 401C35B12244626E0019ED07 /* ApplicationObservation.swift in Sources */, 401C35B7224831470019ED07 /* FocusTransitionCoordinator.swift in Sources */, 40CF37C429BAC18300CDB07A /* ScreensInfo.swift in Sources */, 4062AD371C1FA83300DB612B /* RowLayout.swift in Sources */, 4006CF8E1CDFFE90004CA512 /* AppDelegate.swift in Sources */, 4006CF901CE017BA004CA512 /* UserConfiguration.swift in Sources */, 4062AD2B1C1F99EA00DB612B /* FloatingLayout.swift in Sources */, 401BC8A21CE8D86B00F89B3F /* ShortcutsPreferencesViewController.swift in Sources */, 401BC8A81CE8E94000F89B3F /* WindowsInformation.swift in Sources */, 401BC8981CE7E45300F89B3F /* WindowManager.swift in Sources */, 400A5E9D2327350C00F0A2C3 /* Space.swift in Sources */, 401BC8BA1CE9319500F89B3F /* FocusFollowsMouseManager.swift in Sources */, 40C1357C1F202AEE00FF9FA7 /* PreferencesWindow.swift in Sources */, 40583E1329AEF70B008602BB /* ApplicationEventHandler.swift in Sources */, 4062AD351C1FA62500DB612B /* WideLayout.swift in Sources */, 40A87FFF240AF2BC005EE9C6 /* MousePreferencesViewController.swift in Sources */, 40111CCB22342CC4003D20BD /* DebugPreferencesViewController.swift in Sources */, 401C35B32244650F0019ED07 /* MouseState.swift in Sources */, 4062AD2D1C1F9B8B00DB612B /* FullscreenLayout.swift in Sources */, 40637075224EFED70081299D /* CGInfo.swift in Sources */, 1A4B46EB20AA7717003D5110 /* NSTableView+Amethyst.swift in Sources */, 4045416F268FFDA000861BE8 /* CustomLayout.swift in Sources */, 40637077224F0EFF0081299D /* Screens.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 40D95B431C6E2ED800AAF433 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 403E1A2A2337173600DB7B2A /* FloatingLayoutTests.swift in Sources */, 40111CC9223370FD003D20BD /* SIWindow+AmethystTests.swift in Sources */, 401BBCB62333067F005118F8 /* ColumnLayoutTests.swift in Sources */, 401BC8AE1CE8FD4700F89B3F /* UserConfigurationTests.swift in Sources */, AAAC6BAC2677DF7B00BEC1B0 /* TwoPaneLayoutTests.swift in Sources */, 404BE9D11CFBDF1900D6C537 /* BinarySpacePartitioningLayoutTests.swift in Sources */, 4085824C1EA673AE0075A2C3 /* HotKeyManagerTests.swift in Sources */, 40578C1B232DC607006553A0 /* TestScreen.swift in Sources */, 40B0D145232D2E630021E0A7 /* FullscreenLayoutTests.swift in Sources */, 404541762697C62400861BE8 /* TestBundle.swift in Sources */, 40B0D14B232D944C0021E0A7 /* TestWindow.swift in Sources */, 40D491D823367590007E0CCB /* RowLayoutTests.swift in Sources */, 403E1A2C233719E500DB7B2A /* TallLayoutTests.swift in Sources */, 404541712697C14A00861BE8 /* CustomLayoutTests.swift in Sources */, 4000DB10239CA07000365A0C /* WideLayoutTests.swift in Sources */, 40AB5BEB23AB182300E29346 /* WidescreenTallLayoutTests.swift in Sources */, 40EC47F423F3A30100048B4F /* ScreenManagerTests.swift in Sources */, 40D491DB23367630007E0CCB /* FrameAssignmentVerification.swift in Sources */, 40AB5BED23AC3CF800E29346 /* ThreeColumnLayoutTests.swift in Sources */, 40AB5BE923AB0A2B00E29346 /* TallRightLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 40D95B4D1C6E2ED800AAF433 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 402DB6DD1742E41A00D1C936 /* Amethyst */; targetProxy = 40D95B4C1C6E2ED800AAF433 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 402DB6EA1742E41A00D1C936 /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( 402DB6EB1742E41A00D1C936 /* en */, ); name = InfoPlist.strings; sourceTree = ""; }; 402DB6F01742E41A00D1C936 /* Credits.rtf */ = { isa = PBXVariantGroup; children = ( 402DB6F11742E41A00D1C936 /* en */, ); name = Credits.rtf; sourceTree = ""; }; 402DB6F61742E41A00D1C936 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 401A529824D3B63A004359A4 /* Base */, ); name = MainMenu.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 402DB6F91742E41A00D1C936 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_DYNAMIC_NO_PIC = NO; GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; STRIP_SWIFT_SYMBOLS = NO; SWIFT_VERSION = 5.0; }; name = Debug; }; 402DB6FA1742E41A00D1C936 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = "RELEASE=1"; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; STRIP_SWIFT_SYMBOLS = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; }; name = Release; }; 402DB6FC1742E41A00D1C936 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Amethyst/AmethystDebug.entitlements; CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 128; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_MODULE_VERIFIER = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/Amethyst-hayuzxrdmhaqmverzffhmryaexfi/Build/Products/Debug", "$(PROJECT_DIR)", "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", ); GCC_PRECOMPILE_PREFIX_HEADER = NO; GCC_PREFIX_HEADER = ""; INFOPLIST_FILE = "Amethyst/Amethyst-Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11; MARKETING_VERSION = 0.24.2; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"DEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.amethyst.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; WRAPPER_EXTENSION = app; }; name = Debug; }; 402DB6FD1742E41A00D1C936 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Amethyst/Amethyst.entitlements; CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 128; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 82P2XLB4UH; ENABLE_HARDENED_RUNTIME = YES; ENABLE_MODULE_VERIFIER = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/Amethyst-hayuzxrdmhaqmverzffhmryaexfi/Build/Products/Debug", "$(PROJECT_DIR)", "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", ); GCC_PRECOMPILE_PREFIX_HEADER = NO; GCC_PREFIX_HEADER = ""; GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = "Amethyst/Amethyst-Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11; MARKETING_VERSION = 0.24.2; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"RELEASE\""; PRODUCT_BUNDLE_IDENTIFIER = "com.amethyst.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; WRAPPER_EXTENSION = app; }; name = Release; }; 40D95B4F1C6E2ED800AAF433 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; INFOPLIST_FILE = AmethystTests/Info.plist; MACOSX_DEPLOYMENT_TARGET = 11; MTL_ENABLE_DEBUG_INFO = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.amethyst.AmethystTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "AmethystTests/AmethystTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Amethyst.app/Contents/MacOS/Amethyst"; }; name = Debug; }; 40D95B501C6E2ED800AAF433 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; INFOPLIST_FILE = AmethystTests/Info.plist; MACOSX_DEPLOYMENT_TARGET = 11; MTL_ENABLE_DEBUG_INFO = NO; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.amethyst.AmethystTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "AmethystTests/AmethystTests-Bridging-Header.h"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Amethyst.app/Contents/MacOS/Amethyst"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 402DB6D91742E41A00D1C936 /* Build configuration list for PBXProject "Amethyst" */ = { isa = XCConfigurationList; buildConfigurations = ( 402DB6F91742E41A00D1C936 /* Debug */, 402DB6FA1742E41A00D1C936 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 402DB6FB1742E41A00D1C936 /* Build configuration list for PBXNativeTarget "Amethyst" */ = { isa = XCConfigurationList; buildConfigurations = ( 402DB6FC1742E41A00D1C936 /* Debug */, 402DB6FD1742E41A00D1C936 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 40D95B4E1C6E2ED800AAF433 /* Build configuration list for PBXNativeTarget "AmethystTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 40D95B4F1C6E2ED800AAF433 /* Debug */, 40D95B501C6E2ED800AAF433 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 400D48F22A92AF1E0082750F /* XCRemoteSwiftPackageReference "Cartography" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/robb/Cartography"; requirement = { kind = upToNextMajorVersion; minimumVersion = 4.0.0; }; }; 400D48F52A92AF9B0082750F /* XCRemoteSwiftPackageReference "LoginServiceKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Clipy/LoginServiceKit"; requirement = { branch = master; kind = branch; }; }; 400D48F82A92B0130082750F /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.0.0; }; }; 400D48FB2A92B06A0082750F /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; requirement = { kind = upToNextMajorVersion; minimumVersion = 5.0.0; }; }; 400D48FE2A92B0A00082750F /* XCRemoteSwiftPackageReference "Yams" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/jpsim/Yams"; requirement = { kind = upToNextMajorVersion; minimumVersion = 5.0.0; }; }; 400F2DF12AABF4FC00C1AAE2 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.0.0; }; }; 402DA3D82F53DCFE00B08CA2 /* XCRemoteSwiftPackageReference "silica" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ianyh/silica"; requirement = { branch = master; kind = branch; }; }; 40AE15E12A92E9AF00E14536 /* XCRemoteSwiftPackageReference "SwiftyBeaver" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyBeaver/SwiftyBeaver"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.9.5; }; }; 40AE15E42A92EA7400E14536 /* XCRemoteSwiftPackageReference "RxSwift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ReactiveX/RxSwift"; requirement = { kind = upToNextMajorVersion; minimumVersion = 6.0.0; }; }; 40AE15EC2A92EBD800E14536 /* XCRemoteSwiftPackageReference "Quick" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Quick/Quick"; requirement = { kind = upToNextMajorVersion; minimumVersion = 6.1.0; }; }; 40AE15EF2A92EC5300E14536 /* XCRemoteSwiftPackageReference "Nimble" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Quick/Nimble"; requirement = { kind = upToNextMajorVersion; minimumVersion = 11.2.1; }; }; 40CF37BE29B440A100CDB07A /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-argument-parser.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.0; }; }; 40D540EE2ABFC7560007F40A /* XCRemoteSwiftPackageReference "MASShortcut" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/shpakovski/MASShortcut"; requirement = { branch = master; kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 400D48F32A92AF1E0082750F /* Cartography */ = { isa = XCSwiftPackageProductDependency; package = 400D48F22A92AF1E0082750F /* XCRemoteSwiftPackageReference "Cartography" */; productName = Cartography; }; 400D48F62A92AF9B0082750F /* LoginServiceKit */ = { isa = XCSwiftPackageProductDependency; package = 400D48F52A92AF9B0082750F /* XCRemoteSwiftPackageReference "LoginServiceKit" */; productName = LoginServiceKit; }; 400D48F92A92B0130082750F /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = 400D48F82A92B0130082750F /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; 400D48FC2A92B06B0082750F /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = 400D48FB2A92B06A0082750F /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; 400D48FF2A92B0A00082750F /* Yams */ = { isa = XCSwiftPackageProductDependency; package = 400D48FE2A92B0A00082750F /* XCRemoteSwiftPackageReference "Yams" */; productName = Yams; }; 400F2DF22AABF4FC00C1AAE2 /* KeyboardShortcuts */ = { isa = XCSwiftPackageProductDependency; package = 400F2DF12AABF4FC00C1AAE2 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; productName = KeyboardShortcuts; }; 402DA3D62F53DCDB00B08CA2 /* Silica */ = { isa = XCSwiftPackageProductDependency; productName = Silica; }; 402DA3D92F53DCFE00B08CA2 /* Silica */ = { isa = XCSwiftPackageProductDependency; package = 402DA3D82F53DCFE00B08CA2 /* XCRemoteSwiftPackageReference "silica" */; productName = Silica; }; 40AE15E22A92E9AF00E14536 /* SwiftyBeaver */ = { isa = XCSwiftPackageProductDependency; package = 40AE15E12A92E9AF00E14536 /* XCRemoteSwiftPackageReference "SwiftyBeaver" */; productName = SwiftyBeaver; }; 40AE15E52A92EA7500E14536 /* RxCocoa */ = { isa = XCSwiftPackageProductDependency; package = 40AE15E42A92EA7400E14536 /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxCocoa; }; 40AE15E72A92EA7500E14536 /* RxSwift */ = { isa = XCSwiftPackageProductDependency; package = 40AE15E42A92EA7400E14536 /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxSwift; }; 40AE15ED2A92EBD800E14536 /* Quick */ = { isa = XCSwiftPackageProductDependency; package = 40AE15EC2A92EBD800E14536 /* XCRemoteSwiftPackageReference "Quick" */; productName = Quick; }; 40AE15F02A92EC5300E14536 /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = 40AE15EF2A92EC5300E14536 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; 40CF37BF29B440A100CDB07A /* ArgumentParser */ = { isa = XCSwiftPackageProductDependency; package = 40CF37BE29B440A100CDB07A /* XCRemoteSwiftPackageReference "swift-argument-parser" */; productName = ArgumentParser; }; 40D540EF2ABFC7560007F40A /* MASShortcut */ = { isa = XCSwiftPackageProductDependency; package = 40D540EE2ABFC7560007F40A /* XCRemoteSwiftPackageReference "MASShortcut" */; productName = MASShortcut; }; 40E2D22F2D544F1E00D16B87 /* Silica */ = { isa = XCSwiftPackageProductDependency; productName = Silica; }; 40EFFB492F0226EF00EDD929 /* Silica */ = { isa = XCSwiftPackageProductDependency; productName = Silica; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 402DB6D61742E41A00D1C936 /* Project object */; } ================================================ FILE: Amethyst.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Amethyst.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "originHash" : "0bdfbd20a0602e0ca7fe7531ef1f5ef5df49b4edc989a6121656a1209da2ca9e", "pins" : [ { "identity" : "cartography", "kind" : "remoteSourceControl", "location" : "https://github.com/robb/Cartography", "state" : { "revision" : "b75197ea134f42b5feafb04b526b37eb1a41034b", "version" : "4.0.0" } }, { "identity" : "cwlcatchexception", "kind" : "remoteSourceControl", "location" : "https://github.com/mattgallagher/CwlCatchException.git", "state" : { "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", "version" : "2.1.2" } }, { "identity" : "cwlpreconditiontesting", "kind" : "remoteSourceControl", "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", "state" : { "revision" : "a23ded2c91df9156628a6996ab4f347526f17b6b", "version" : "2.1.2" } }, { "identity" : "keyboardshortcuts", "kind" : "remoteSourceControl", "location" : "https://github.com/sindresorhus/KeyboardShortcuts", "state" : { "revision" : "1aef85578fdd4f9eaeeb8d53b7b4fc31bf08fe27", "version" : "2.4.0" } }, { "identity" : "loginservicekit", "kind" : "remoteSourceControl", "location" : "https://github.com/Clipy/LoginServiceKit", "state" : { "branch" : "master", "revision" : "a8e68051aca8bbb702e62ab36006a301966ab053" } }, { "identity" : "masshortcut", "kind" : "remoteSourceControl", "location" : "https://github.com/shpakovski/MASShortcut", "state" : { "branch" : "master", "revision" : "6f2603c6b6cc18f64a799e5d2c9d3bbc467c413a" } }, { "identity" : "nimble", "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Nimble", "state" : { "revision" : "eb5e3d717224fa0d1f6aff3fc2c5e8e81fa1f728", "version" : "11.2.2" } }, { "identity" : "quick", "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Quick", "state" : { "revision" : "16910e406be96e08923918315388c3e989deac9e", "version" : "6.1.0" } }, { "identity" : "rxswift", "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveX/RxSwift", "state" : { "revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4", "version" : "6.6.0" } }, { "identity" : "sparkle", "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { "revision" : "f0ceaf5cc9f3f23daa0ccb6dcebd79fc96ccc7d9", "version" : "2.5.0" } }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", "version" : "1.2.3" } }, { "identity" : "swiftybeaver", "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver", "state" : { "revision" : "12b5acf96d98f91d50de447369bd18df74600f1a", "version" : "1.9.6" } }, { "identity" : "swiftyjson", "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftyJSON/SwiftyJSON", "state" : { "revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", "version" : "5.0.1" } }, { "identity" : "yams", "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams", "state" : { "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", "version" : "5.0.6" } } ], "version" : 3 } ================================================ FILE: Amethyst.xcodeproj/xcshareddata/xcschemes/Amethyst Debug CLI.xcscheme ================================================ ================================================ FILE: Amethyst.xcodeproj/xcshareddata/xcschemes/Amethyst.xcscheme ================================================ ================================================ FILE: Amethyst.xctestplan ================================================ { "configurations" : [ { "id" : "3A3BA42F-5862-48E3-9505-9D3676A1BA59", "name" : "Standard", "options" : { "commandLineArgumentEntries" : [ { "argument" : "test" } ] } } ], "defaultOptions" : { "targetForVariableExpansion" : { "containerPath" : "container:Amethyst.xcodeproj", "identifier" : "402DB6DD1742E41A00D1C936", "name" : "Amethyst" } }, "testTargets" : [ { "target" : { "containerPath" : "container:Amethyst.xcodeproj", "identifier" : "40D95B461C6E2ED800AAF433", "name" : "AmethystTests" } } ], "version" : 1 } ================================================ FILE: Amethyst.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Amethyst.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Amethyst.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "originHash" : "5c090c150afce6ddb786afef9aa71f0237da58318f433cf9eea579c374d84d51", "pins" : [ { "identity" : "cartography", "kind" : "remoteSourceControl", "location" : "https://github.com/robb/Cartography", "state" : { "revision" : "b75197ea134f42b5feafb04b526b37eb1a41034b", "version" : "4.0.0" } }, { "identity" : "cwlcatchexception", "kind" : "remoteSourceControl", "location" : "https://github.com/mattgallagher/CwlCatchException.git", "state" : { "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", "version" : "2.1.2" } }, { "identity" : "cwlpreconditiontesting", "kind" : "remoteSourceControl", "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", "state" : { "revision" : "a23ded2c91df9156628a6996ab4f347526f17b6b", "version" : "2.1.2" } }, { "identity" : "keyboardshortcuts", "kind" : "remoteSourceControl", "location" : "https://github.com/sindresorhus/KeyboardShortcuts", "state" : { "revision" : "7ecc38bb6edf7d087d30e737057b8d8a9b7f51eb", "version" : "2.2.4" } }, { "identity" : "loginservicekit", "kind" : "remoteSourceControl", "location" : "https://github.com/Clipy/LoginServiceKit", "state" : { "branch" : "master", "revision" : "a8e68051aca8bbb702e62ab36006a301966ab053" } }, { "identity" : "masshortcut", "kind" : "remoteSourceControl", "location" : "https://github.com/shpakovski/MASShortcut", "state" : { "branch" : "master", "revision" : "6f2603c6b6cc18f64a799e5d2c9d3bbc467c413a" } }, { "identity" : "nimble", "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Nimble", "state" : { "revision" : "eb5e3d717224fa0d1f6aff3fc2c5e8e81fa1f728", "version" : "11.2.2" } }, { "identity" : "quick", "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Quick", "state" : { "revision" : "16910e406be96e08923918315388c3e989deac9e", "version" : "6.1.0" } }, { "identity" : "rxswift", "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveX/RxSwift", "state" : { "revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4", "version" : "6.6.0" } }, { "identity" : "silica", "kind" : "remoteSourceControl", "location" : "https://github.com/ianyh/silica", "state" : { "branch" : "master", "revision" : "237b2aeba4e59279e2d4e2b0fe637994f9823257" } }, { "identity" : "sparkle", "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { "revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99", "version" : "2.7.0" } }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "41982a3656a71c768319979febd796c6fd111d5c", "version" : "1.5.0" } }, { "identity" : "swiftybeaver", "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver", "state" : { "revision" : "12b5acf96d98f91d50de447369bd18df74600f1a", "version" : "1.9.6" } }, { "identity" : "swiftyjson", "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftyJSON/SwiftyJSON", "state" : { "revision" : "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828", "version" : "5.0.2" } }, { "identity" : "yams", "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams", "state" : { "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", "version" : "5.1.3" } } ], "version" : 3 } ================================================ FILE: AmethystTests/AmethystTests-Bridging-Header.h ================================================ ================================================ FILE: AmethystTests/Helpers/FrameAssignmentVerification.swift ================================================ // // FrameAssignmentVerification.swift // AmethystTests // // Created by Ian Ynda-Hummel on 9/21/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Foundation import Nimble extension RandomAccessCollection where Element == FrameAssignmentOperation, Index == Int { func filtered(byIDs ids: [TestWindow.WindowID]) -> [Element] { return filter { ids.contains($0.frameAssignment.window.id) } } func forWindows(_ windows: C) -> [Element] where C.Element == TestWindow, C.Index == Index { return filtered(byIDs: Array(windows).map { $0.id() }) } func sorted() -> [Element] { return sorted { $0.frameAssignment.frame.origin.x < $1.frameAssignment.frame.origin.x } .sorted { $0.frameAssignment.frame.origin.y < $1.frameAssignment.frame.origin.y } } func frames() -> [CGRect] { return map { $0.frameAssignment.frame } } func description(withExpectedFrames frames: [CGRect]) -> String { return zip(self.map { $0.frameAssignment }, frames).map { assignment, frame in return "\(assignment.window.id):\n\tFrame: \(assignment.frame)\n\tExpected: \(frame)" }.joined(separator: "\n") } func verify(frames: [CGRect], inOrder: Bool = false) { expect(self.count).to(equal(frames.count), description: "\(count) assignments, but \(frames.count) frames") if inOrder { zip(self.map { $0.frameAssignment }, frames).forEach { assignment, frame in expect(assignment.frame).to(equal(frame)) } } else { let currentFrames = map { $0.frameAssignment.frame } for frame in frames { expect(currentFrames).to(contain(frame)) } } } func verify(frames: [String: CGRect]) { var unverifiedFrames = frames for operation in self { let id = operation.frameAssignment.window.id expect(unverifiedFrames[id]).toNot(beNil(), description: "\(id) should exist") expect(operation.frameAssignment.frame).to(equal(unverifiedFrames[id]), description: "\(id)") unverifiedFrames[id] = nil } expect(unverifiedFrames).to(beEmpty()) } } ================================================ FILE: AmethystTests/Helpers/TestBundle.swift ================================================ // // TestBundle.swift // AmethystTests // // Created by Ian Ynda-Hummel on 7/8/21. // Copyright © 2021 Ian Ynda-Hummel. All rights reserved. // import Foundation extension Bundle { static var testBundle: Bundle { return Bundle(for: TestWindow.self) } static func layoutFile(key: String) -> URL? { return testBundle.path(forResource: key, ofType: "js").flatMap { URL(fileURLWithPath: $0) } } } ================================================ FILE: AmethystTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: AmethystTests/Model/CustomLayouts/extended.js ================================================ function layout() { return { name: "Extended Tall", extends: "tall", getFrameAssignments: (windows, screenFrame, state, extendedFrames) => { const maxX = Math.max(...extendedFrames.map(f => f.frame.x)); const minX = Math.min(...extendedFrames.map(f => f.frame.x)); return extendedFrames.reduce((frames, extendedFrame) => { const frame = { x: extendedFrame.frame.x === minX ? maxX : minX, y: extendedFrame.frame.y, width: extendedFrame.frame.width, height: extendedFrame.frame.height }; return { ...frames, [extendedFrame.id]: frame }; }, {}); } }; } ================================================ FILE: AmethystTests/Model/CustomLayouts/fullscreen.js ================================================ function layout() { return { name: "Fullscreen", getFrameAssignments: (windows, screenFrame) => { return windows.reduce((frames, w) => ({ ...frames, [w.id]: screenFrame }), {}); } }; } ================================================ FILE: AmethystTests/Model/CustomLayouts/null.js ================================================ function layout() { return { name: "Null", getFrameAssignments: null }; } ================================================ FILE: AmethystTests/Model/CustomLayouts/recommended-main-pane-ratio.js ================================================ function layout() { return { name: "Ratio", initialState: { mainPaneRatio: 0.5 }, recommendMainPaneRatio: (ratio, state) => { return { ...state, mainPaneRatio: ratio }; }, getFrameAssignments: (windows, screenFrame, state) => { return windows.reduce((frames, window, index) => { if (index === 0) { const frame = { x: screenFrame.x, y: screenFrame.y, width: screenFrame.width * state.mainPaneRatio, height: screenFrame.height, isMain: true, unconstrainedDimension: "horizontal" }; return { ...frames, [window.id]: frame }; } else { const frame = { x: screenFrame.x + screenFrame.width * state.mainPaneRatio, y: screenFrame.y, width: screenFrame.width - screenFrame.width * state.mainPaneRatio, height: screenFrame.height, isMain: false, unconstrainedDimension: "horizontal" }; return { ...frames, [window.id]: frame }; } }, {}); } }; } ================================================ FILE: AmethystTests/Model/CustomLayouts/static-ratio-tall-native-commands.js ================================================ function layout() { return { name: "Static Ratio Tall with Native Commands", initialState: { mainPaneCount: 1 }, commands: { increaseMain: { description: "Increase main pane count", updateState: (state) => { return { ...state, mainPaneCount: state.mainPaneCount + 1 }; } }, decreaseMain: { description: "Decrease main pane count", updateState: (state) => { return { ...state, mainPaneCount: Math.max(1, state.mainPaneCount - 1) }; } } }, getFrameAssignments: (windows, screenFrame, state) => { const mainPaneCount = Math.min(state.mainPaneCount, windows.length); const secondaryPaneCount = windows.length - mainPaneCount; const hasSecondaryPane = secondaryPaneCount > 0; const mainPaneWindowHeight = Math.round(screenFrame.height / mainPaneCount); const secondaryPaneWindowHeight = Math.round(hasSecondaryPane ? (screenFrame.height / secondaryPaneCount) : 0); return windows.reduce((frames, window, index) => { const isMain = index < mainPaneCount; let frame; if (isMain) { frame = { x: screenFrame.x, y: screenFrame.y + mainPaneWindowHeight * index, width: screenFrame.width / 2, height: mainPaneWindowHeight }; } else { frame = { x: screenFrame.x + screenFrame.width / 2, y: screenFrame.y + secondaryPaneWindowHeight * (index - mainPaneCount), width: screenFrame.width / 2, height: secondaryPaneWindowHeight } } return { ...frames, [window.id]: frame }; }, {}); } }; } ================================================ FILE: AmethystTests/Model/CustomLayouts/static-ratio-tall.js ================================================ function layout() { return { name: "Static Ratio Tall", initialState: { mainPaneCount: 1 }, commands: { command3: { description: "Increase main pane count", updateState: (state) => { return { ...state, mainPaneCount: state.mainPaneCount + 1 }; } }, command4: { description: "Decrease main pane count", updateState: (state) => { return { ...state, mainPaneCount: Math.max(1, state.mainPaneCount - 1) }; } } }, getFrameAssignments: (windows, screenFrame, state) => { const mainPaneCount = Math.min(state.mainPaneCount, windows.length); const secondaryPaneCount = windows.length - mainPaneCount; const hasSecondaryPane = secondaryPaneCount > 0; const mainPaneWindowHeight = Math.round(screenFrame.height / mainPaneCount); const secondaryPaneWindowHeight = Math.round(hasSecondaryPane ? (screenFrame.height / secondaryPaneCount) : 0); return windows.reduce((frames, window, index) => { const isMain = index < mainPaneCount; let frame; if (isMain) { frame = { x: screenFrame.x, y: screenFrame.y + mainPaneWindowHeight * index, width: screenFrame.width / 2, height: mainPaneWindowHeight }; } else { frame = { x: screenFrame.x + screenFrame.width / 2, y: screenFrame.y + secondaryPaneWindowHeight * (index - mainPaneCount), width: screenFrame.width / 2, height: secondaryPaneWindowHeight } } return { ...frames, [window.id]: frame }; }, {}); } }; } ================================================ FILE: AmethystTests/Model/CustomLayouts/subset.js ================================================ function layout() { return { name: "Subset", initialState: { ids: [] }, commands: { command3: { description: "Add window to subset", updateState: (state, focusedWindowID) => { const ids = state.ids; if (!!focusedWindowID) { const index = ids.indexOf(focusedWindowID); if (index === -1) { ids.push(focusedWindowID); } } return { ...state, ids }; } }, command4: { description: "Remove window from subset", updateState: (state, focusedWindowID) => { const ids = state.ids; if (!!focusedWindowID) { const index = ids.indexOf(focusedWindowID); if (index > -1) { ids.splice(index, 1); } } return { ...state, ids }; } } }, getFrameAssignments: (windows, screenFrame, state) => { const mainPaneCount = state.ids.length; const secondaryPaneCount = Math.max(windows.length - mainPaneCount, 0); const hasSecondaryPane = secondaryPaneCount > 0; const mainPaneWindowWidth = hasSecondaryPane ? screenFrame.width / 2 : screenFrame.width; const mainPaneWindowHeight = Math.round(screenFrame.height / mainPaneCount); const secondaryPaneWindowHeight = Math.round(hasSecondaryPane ? (screenFrame.height / secondaryPaneCount) : 0); let mainIndex = 0; let secondaryIndex = 0; return windows.reduce((frames, window) => { const isMain = state.ids.includes(window.id); let frame; if (isMain) { frame = { x: screenFrame.x, y: screenFrame.y + mainPaneWindowHeight * mainIndex, width: mainPaneWindowWidth, height: mainPaneWindowHeight }; mainIndex++; } else { frame = { x: screenFrame.x + screenFrame.width / 2, y: screenFrame.y + secondaryPaneWindowHeight * secondaryIndex, width: screenFrame.width / 2, height: secondaryPaneWindowHeight }; secondaryIndex++; } return { ...frames, [window.id]: frame }; }, {}); }, updateWithChange: (change, state) => { switch (change.change) { case "window_swap": if (state.ids.includes(change.windowID) && !state.ids.includes(change.otherWindowID)) { const index = state.ids.indexOf(change.windowID); state.ids.splice(index, 1); state.ids.push(change.otherWindowID); } else if (state.ids.includes(change.otherWindowID) && !state.ids.includes(change.windowID)) { const index = state.ids.indexOf(change.otherWindowID); state.ids.splice(index, 1); state.ids.push(change.windowID); } break; } return state; } }; } ================================================ FILE: AmethystTests/Model/CustomLayouts/undefined.js ================================================ function layout() { return { name: "Undefined" }; } ================================================ FILE: AmethystTests/Model/CustomLayouts/uniform-columns.js ================================================ function layout() { return { name: "Uniform Columns", getFrameAssignments: (windows, screenFrame) => { const columnWidth = screenFrame.width / windows.length; const frames = windows.map((window, index) => { const frame = { x: screenFrame.x + (columnWidth * index), y: screenFrame.y, width: columnWidth, height: screenFrame.height }; return { [window.id]: frame }; }); return frames.reduce((frames, frame) => ({ ...frames, ...frame }), {}); } }; } ================================================ FILE: AmethystTests/Model/TestScreen.swift ================================================ // // TestScreen.swift // AmethystTests // // Created by Ian Ynda-Hummel on 9/14/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Foundation import SwiftyJSON final class TestScreen: ScreenType { static var availableScreens: [TestScreen] = [] static var screensHaveSeparateSpaces = true static func screenDescriptions() -> [JSON]? { return [] } private let id: String = UUID().uuidString private let internalFrame: CGRect init(frame: CGRect) { internalFrame = frame } convenience init() { let frame = CGRect(x: 0, y: 0, width: round(CGFloat.random(in: 500...2000)), height: round(CGFloat.random(in: 500...2000))) self.init(frame: frame) } func adjustedFrame(disableWindowMargins: Bool) -> CGRect { return internalFrame } func frameIncludingDockAndMenu() -> CGRect { return internalFrame } func frameWithoutDockOrMenu() -> CGRect { return internalFrame } func frame() -> CGRect { return internalFrame } func screenID() -> String? { return id } func focusScreen() { } static func == (lhs: TestScreen, rhs: TestScreen) -> Bool { return lhs.id == rhs.id } } ================================================ FILE: AmethystTests/Model/TestWindow.swift ================================================ // // TestWindow.swift // AmethystTests // // Created by Ian Ynda-Hummel on 9/14/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Foundation import Silica final class TestWindow: WindowType { typealias Screen = TestScreen typealias WindowID = String static var focused: TestWindow? private let element: SIAccessibilityElement? private let cgWindowID = CGWindowID(Int.random(in: 1...1000)) private let uuid = UUID().uuidString private var _frame: CGRect = .zero var isFocusedValue = false static func currentlyFocused() -> Self? { return (focused as? Self) } required init?(element: SIAccessibilityElement?) { self.element = element } func id() -> WindowID { return uuid } func cgID() -> CGWindowID { return cgWindowID } func frame() -> CGRect { return _frame } func screen() -> Screen? { return nil } func setFrame(_ frame: CGRect, withThreshold threshold: CGSize) { _frame = frame } func isFocused() -> Bool { return isFocusedValue } func pid() -> pid_t { return pid_t(1234) } func title() -> String? { return nil } func shouldBeManaged() -> Bool { return true } func shouldFloat() -> Bool { return false } func isActive() -> Bool { return true } func focus() -> Bool { return false } func minimize() -> Bool { return false } func moveScaled(to screen: Screen) { } func isOnScreen() -> Bool { return true } func move(toSpace space: UInt) { } func move(toSpaceAtIndex space: UInt) { } func move(toSpace spaceID: CGSSpaceID) { } static func == (lhs: TestWindow, rhs: TestWindow) -> Bool { return lhs.id() == rhs.id() } } ================================================ FILE: AmethystTests/Tests/Categories/SIWindow+AmethystTests.swift ================================================ @testable import Amethyst import Cocoa import Nimble import Quick class SIWindowAmethystTests: QuickSpec { override func spec() { describe("approximate rect comparison") { it("tolerates some provided error") { let rect = CGRect(x: 100, y: 100, width: 100, height: 100) let tolerance = CGRect(x: 10, y: 10, width: 10, height: 10) let translations = [ CGAffineTransform(translationX: 5, y: 5), CGAffineTransform(translationX: 5, y: 0), CGAffineTransform(translationX: 5, y: -5), CGAffineTransform(translationX: 0, y: 5), CGAffineTransform(translationX: 0, y: -5), CGAffineTransform(translationX: -5, y: 5), CGAffineTransform(translationX: -5, y: 0), CGAffineTransform(translationX: -5, y: -5), CGAffineTransform(scaleX: 1.05, y: 1.05), CGAffineTransform(scaleX: 1.05, y: 0.95), CGAffineTransform(scaleX: 0.95, y: 1.05), CGAffineTransform(scaleX: 0.95, y: 0.95) ] translations.forEach { expect(rect.applying($0).approximatelyEqual(to: rect, within: tolerance)).to(beTrue(), description: "\($0)") } } } } } ================================================ FILE: AmethystTests/Tests/Configuration/UserConfigurationTests.swift ================================================ // // UserConfigurationTests.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/15/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Cocoa import Nimble import Quick import SwiftyJSON import Yams class TestConfigurationStorage: ConfigurationStorage { var storage: [ConfigurationKey: Any] = [:] func object(forKey key: ConfigurationKey) -> Any? { return storage[key] } func array(forKey key: ConfigurationKey) -> [Any]? { return storage[key] as? [Any] } func bool(forKey key: ConfigurationKey) -> Bool { return (storage[key] as? Bool) ?? false } func float(forKey key: ConfigurationKey) -> Float { return (storage[key] as? Float) ?? 0 } func stringArray(forKey key: ConfigurationKey) -> [String]? { return storage[key] as? [String] } func set(_ value: Any?, forKey key: ConfigurationKey) { storage[key] = value } func set(_ value: Bool, forKey key: ConfigurationKey) { storage[key] = value } } class UserConfigurationTests: QuickSpec { private class TestHotKeyRegistrar: HotKeyRegistrar { private(set) var keyString: String? private(set) var modifiers: AMModifierFlags? private(set) var handler: (() -> Void)? private(set) var defaultsKey: String? private(set) var override: Bool? init() {} func registerHotKey(with string: String?, modifiers: AMModifierFlags?, handler: @escaping () -> Void, defaultsKey: String, override: Bool) { keyString = string self.modifiers = modifiers self.handler = handler self.defaultsKey = defaultsKey self.override = override } } private class TestBundleIdentifiable: BundleIdentifiable { var bundleIdentifier: String? } override func spec() { describe("constructing commands") { context("overrides") { it("when user configuration exists") { var configuration = UserConfiguration(storage: TestConfigurationStorage()) let localConfiguration: [String: Any] = [ "test": [ "mod": "mod1", "key": "1" ] ] configuration.configurationYAML = nil configuration.configurationJSON = JSON(localConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) var registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(registrar.override).to(beTrue()) configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.configurationYAML = localConfiguration configuration.configurationJSON = nil configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(registrar.override).to(beTrue()) } it("when custom mod1 is specified") { var configuration = UserConfiguration(storage: TestConfigurationStorage()) let localConfiguration: [String: Any] = [ "mod1": [ "command" ] ] let defaultConfiguration: [String: Any] = [ "test": [ "mod": "mod1", "key": "1" ] ] configuration.configurationYAML = nil configuration.configurationJSON = JSON(localConfiguration) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) var registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(registrar.override).to(beTrue()) configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.configurationYAML = localConfiguration configuration.configurationJSON = nil configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(registrar.override).to(beTrue()) } it("when custom mod2 is specified") { var configuration = UserConfiguration(storage: TestConfigurationStorage()) let localConfiguration: [String: Any] = [ "mod2": [ "command" ] ] let defaultConfiguration: [String: Any] = [ "test": [ "mod": "mod1", "key": "1" ] ] configuration.configurationJSON = JSON(localConfiguration) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) var registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(registrar.override).to(beTrue()) configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.configurationYAML = localConfiguration configuration.configurationJSON = nil configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(registrar.override).to(beTrue()) } } context("takes command") { it("from local configuration over default") { var configuration = UserConfiguration(storage: TestConfigurationStorage()) let localConfiguration: [String: Any] = [ "test": [ "mod": "mod1", "key": "1" ] ] let defaultConfiguration: [String: Any] = [ "test": [ "mod": "mod1", "key": "2" ] ] configuration.configurationJSON = JSON(localConfiguration) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) var registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(registrar.keyString).to(equal("1")) configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.configurationJSON = JSON(localConfiguration) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(registrar.keyString).to(equal("1")) } it("from default in absence of local configuration") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) let defaultConfiguration: [String: Any] = [ "test": [ "mod": "mod1", "key": "1" ] ] configuration.configurationJSON = JSON([:]) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) let registrar = TestHotKeyRegistrar() configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) expect(configuration.defaultConfiguration?["test"]["key"].string).to(equal("1")) expect(registrar.keyString).to(equal("1")) } } it("does not crash for malformed commands") { var configuration = UserConfiguration(storage: TestConfigurationStorage()) let localConfiguration: [String: Any] = [ "test": [ "key": "2" ] ] let defaultConfiguration: [String: Any] = [ "test": [ "mod": "mod1", "key": "2" ] ] configuration.configurationYAML = nil configuration.configurationJSON = JSON(localConfiguration) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) var registrar = TestHotKeyRegistrar() expect { configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) }.toNot(throwError()) configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.configurationYAML = localConfiguration configuration.configurationJSON = nil configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.modifier1 = configuration.modifierFlagsForStrings(["command"]) registrar = TestHotKeyRegistrar() expect { configuration.constructCommand(for: registrar, commandKey: "test", handler: {}) }.toNot(throwError()) } } describe("floating application") { it("is not floating by default") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set([] as Any?, forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.notFloating))) } it("floats for exact matches") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set(["test.test.Test"], forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.floating))) } it("floats for wildcard matches") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set(["test.test.*"], forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.floating))) } it("floats for prefixed wildcard matches") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set(["*.Test"], forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.floating))) } it("floats for inline wildcard matches") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set(["test.*.Test"], forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.foo.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.floating))) } it("does not float for exact mismatches") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set(["test.test.Other"], forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.notFloating))) } it("does not float for wildcard mismatches") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set(["test.other.*"], forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.notFloating))) } context("as whitelist") { it("does not float for a matching window title") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let floatingBundle = FloatingBundle(id: "test.test.Test", windowTitles: ["test"]) storage.set(false, forKey: .floatingBundleIdentifiersIsBlacklist) configuration.setFloatingBundles([floatingBundle]) let bundleIdentifiable = TestBundleIdentifiable() let title = "test" bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.notFloating))) } it("does not float for a matching application with no specified window titles") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(false, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set(["test.test.Test"], forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title)).to(equal(.reliable(.notFloating))) } it("floats for no specified applications") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(false, forKey: .floatingBundleIdentifiersIsBlacklist) storage.set([], forKey: .floatingBundleIdentifiers) let bundleIdentifiable = TestBundleIdentifiable() let title = UUID().uuidString bundleIdentifiable.bundleIdentifier = "test.test.Test" let float = configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: title) expect(float).to(equal(.reliable(.floating))) } } context("specified window titles") { it("only float windows with titles") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let floatingBundle = FloatingBundle(id: "test.test.Test", windowTitles: ["test1"]) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) configuration.setFloatingBundles([floatingBundle]) let bundleIdentifiable = TestBundleIdentifiable() bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: "test1")).to(equal(.reliable(.floating))) expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: "test2")).to(equal(.reliable(.notFloating))) } it("only allow windows with titles") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let floatingBundle = FloatingBundle(id: "test.test.Test", windowTitles: ["test1"]) storage.set(false, forKey: .floatingBundleIdentifiersIsBlacklist) configuration.setFloatingBundles([floatingBundle]) let bundleIdentifiable = TestBundleIdentifiable() bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: "test1")).to(equal(.reliable(.notFloating))) expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: "test2")).to(equal(.reliable(.floating))) } it("treats empty and nil titles as unreliable") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let floatingBundle = FloatingBundle(id: "test.test.Test", windowTitles: ["test1"]) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) configuration.setFloatingBundles([floatingBundle]) let bundleIdentifiable = TestBundleIdentifiable() bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: "")).to(equal(.unreliable(.notFloating))) expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: nil)).to(equal(.unreliable(.notFloating))) storage.set(false, forKey: .floatingBundleIdentifiersIsBlacklist) expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: "")).to(equal(.unreliable(.floating))) expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: nil)).to(equal(.unreliable(.floating))) } it("treats empty and nil titles as reliable if no title would match anyway") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .floatingBundleIdentifiersIsBlacklist) configuration.setFloatingBundles([]) let bundleIdentifiable = TestBundleIdentifiable() bundleIdentifiable.bundleIdentifier = "test.test.Test" expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: "")).to(equal(.reliable(.notFloating))) expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: nil)).to(equal(.reliable(.notFloating))) storage.set(false, forKey: .floatingBundleIdentifiersIsBlacklist) expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: "")).to(equal(.reliable(.floating))) expect(configuration.runningApplication(bundleIdentifiable, byDefaultFloatsForTitle: nil)).to(equal(.reliable(.floating))) } } } describe("focus follows mouse") { it("toggles") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(true, forKey: .focusFollowsMouse) expect(configuration.focusFollowsMouse()).to(beTrue()) configuration.toggleFocusFollowsMouse() expect(configuration.focusFollowsMouse()).to(beFalse()) } } describe("small window size") { it("returns default value when not set") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) expect(configuration.smallWindowSize()).to(equal(500)) } it("returns configured value when set") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(Float(300), forKey: .smallWindowSize) expect(configuration.smallWindowSize()).to(equal(300)) } it("returns default value when set to zero") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(Float(0), forKey: .smallWindowSize) expect(configuration.smallWindowSize()).to(equal(500)) } it("returns default value when set to negative") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) storage.set(Float(-100), forKey: .smallWindowSize) expect(configuration.smallWindowSize()).to(equal(500)) } } describe("load configuration") { it("default configuration does not override existing configuration") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let existingLayouts = ["wide"] let defaultConfiguration = [ "layouts": [ "tall" ] ] storage.set(existingLayouts, forKey: .layouts) expect(configuration.layoutKeys()).to(equal(existingLayouts)) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.loadConfiguration() expect(configuration.layoutKeys()).to(equal(existingLayouts)) } it("local json configuration does override existing configuration") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let existingLayouts = ["wide"] let localConfiguration = [ "layouts": [ "fullscreen" ] ] let defaultConfiguration = [ "layouts": [ "tall" ] ] storage.set(existingLayouts, forKey: .layouts) expect(configuration.layoutKeys()).to(equal(existingLayouts)) configuration.configurationYAML = nil configuration.configurationJSON = JSON(localConfiguration) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.loadConfiguration() expect(configuration.layoutKeys()).to(equal(localConfiguration["layouts"])) } it("local yaml configuration does override existing configuration") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let existingLayouts = ["wide"] let localConfiguration = [ "layouts": [ "fullscreen" ] ] let defaultConfiguration = [ "layouts": [ "tall" ] ] storage.set(existingLayouts, forKey: .layouts) expect(configuration.layoutKeys()).to(equal(existingLayouts)) configuration.configurationYAML = localConfiguration configuration.configurationJSON = nil configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.loadConfiguration() expect(configuration.layoutKeys()).to(equal(localConfiguration["layouts"])) } it("prefers yaml over json for local configuration") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) let yamlConfiguration = [ "layouts": [ "fullscreen" ] ] let jsonConfiguration = [ "layouts": [ "tall" ] ] let defaultConfiguration = [ "layouts": [ "wide" ] ] configuration.configurationYAML = yamlConfiguration configuration.configurationJSON = JSON(jsonConfiguration) configuration.defaultConfiguration = JSON(defaultConfiguration) configuration.loadConfiguration() expect(configuration.layoutKeys()).to(equal(yamlConfiguration["layouts"])) } } describe("floating bundles") { describe("returned") { it("handles both strings and objects") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let bundlesData: [Any] = [ "test.test.1", [ "id": "test.test.2", "window-titles": [ "dialog" ] ], "test.test.3" ] storage.set(bundlesData, forKey: .floatingBundleIdentifiers) let bundles = configuration.floatingBundles() let expectedBundles = [ FloatingBundle(id: "test.test.1", windowTitles: []), FloatingBundle(id: "test.test.2", windowTitles: ["dialog"]), FloatingBundle(id: "test.test.3", windowTitles: []) ] expect(bundles.count).to(equal(3)) expect(bundles).to(equal(expectedBundles)) } } describe("set") { it("assigns bundles") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let bundles = [ FloatingBundle(id: "test.test.1", windowTitles: []), FloatingBundle(id: "test.test.2", windowTitles: ["dialog"]), FloatingBundle(id: "test.test.3", windowTitles: []) ] configuration.setFloatingBundles(bundles) let bundlesData = storage.array(forKey: .floatingBundleIdentifiers).flatMap { JSON($0) } let expectedBundlesData: JSON = JSON([ [ "id": "test.test.1", "window-titles": [] ], [ "id": "test.test.2", "window-titles": [ "dialog" ] ], [ "id": "test.test.3", "window-titles": [] ] ]) expect(bundlesData).to(equal(expectedBundlesData)) } } } describe("floating bundles") { describe("returned") { it("handles both strings and objects") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let bundlesData: [Any] = [ // Normal string "test.test.1", // JSON formatted [ "id": "test.test.2", "window-titles": [ "dialog" ] ], // Another string "test.test.3", // YAML formatted [ "test.test.4": [ "window-titles": [ "dialog2" ] ] ] ] storage.set(bundlesData, forKey: .floatingBundleIdentifiers) let bundles = configuration.floatingBundles() let expectedBundles = [ FloatingBundle(id: "test.test.1", windowTitles: []), FloatingBundle(id: "test.test.2", windowTitles: ["dialog"]), FloatingBundle(id: "test.test.3", windowTitles: []), FloatingBundle(id: "test.test.4", windowTitles: ["dialog2"]) ] expect(bundles.count).to(equal(4)) expect(bundles).to(equal(expectedBundles)) } } describe("set") { it("assigns bundles") { let storage = TestConfigurationStorage() let configuration = UserConfiguration(storage: storage) let bundles = [ FloatingBundle(id: "test.test.1", windowTitles: []), FloatingBundle(id: "test.test.2", windowTitles: ["dialog"]), FloatingBundle(id: "test.test.3", windowTitles: []) ] configuration.setFloatingBundles(bundles) let bundlesData = storage.array(forKey: .floatingBundleIdentifiers).flatMap { JSON($0) } let expectedBundlesData: JSON = JSON([ [ "id": "test.test.1", "window-titles": [] ], [ "id": "test.test.2", "window-titles": [ "dialog" ] ], [ "id": "test.test.3", "window-titles": [] ] ]) expect(bundlesData).to(equal(expectedBundlesData)) } } } } } ================================================ FILE: AmethystTests/Tests/Layout/BinarySpacePartitioningLayoutTests.swift ================================================ // // BinarySpacePartitioningLayoutTests.swift // Amethyst // // Created by Ian Ynda-Hummel on 5/29/16. // Copyright © 2016 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Cocoa import Nimble import Quick class BinarySpacePartitioningLayoutTests: QuickSpec { override func spec() { describe("TreeNode") { describe("finding") { it("finds a node that exists") { let rootNode = TreeNode() let node1 = TreeNode() let node2 = TreeNode() let node3 = TreeNode() let node4 = TreeNode() node1.parent = rootNode node1.left = node2 node1.right = node3 node2.windowID = "0" node2.parent = node1 node3.windowID = "1" node3.parent = node1 node4.windowID = "2" node4.parent = rootNode rootNode.left = node1 rootNode.right = node4 expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.findWindowID("0")).to(equal(node2)) expect(rootNode.findWindowID("1")).to(equal(node3)) expect(rootNode.findWindowID("2")).to(equal(node4)) } it("does not find a node that does not exist") { let rootNode = TreeNode() let node1 = TreeNode() let node2 = TreeNode() node1.windowID = "0" node1.parent = rootNode node2.windowID = "1" node2.parent = rootNode rootNode.left = node1 rootNode.right = node2 expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.findWindowID("2")).to(beNil()) } } describe("traversing") { it("generates an empty list for an empty tree") { let rootNode = TreeNode() expect(rootNode.orderedWindowIDs()).to(equal([])) } it("generates a correctly ordered list") { let rootNode = TreeNode() let node1 = TreeNode() let node2 = TreeNode() let node3 = TreeNode() let node4 = TreeNode() node1.parent = rootNode node1.left = node2 node1.right = node3 node2.windowID = "0" node2.parent = node1 node3.windowID = "1" node3.parent = node1 node4.windowID = "2" node4.parent = rootNode rootNode.left = node4 rootNode.right = node1 let orderedList = rootNode.orderedWindowIDs() expect(orderedList).to(equal(["2", "0", "1"])) } } describe("insertion") { it("inserts at end") { let rootNode = TreeNode() let node1 = TreeNode() let node2 = TreeNode() node1.windowID = "0" node1.parent = rootNode node2.windowID = "1" node2.parent = rootNode rootNode.left = node1 rootNode.right = node2 expect(rootNode.treeIsValid()).to(beTrue()) rootNode.insertWindowIDAtEnd("2") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.right?.left?.windowID).to(equal("1")) expect(rootNode.right?.right?.windowID).to(equal("2")) rootNode.insertWindowIDAtEnd("3") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.right?.left?.windowID).to(equal("1")) expect(rootNode.right?.right?.left?.windowID).to(equal("2")) expect(rootNode.right?.right?.right?.windowID).to(equal("3")) } it("inserts at the insertion point") { let rootNode = TreeNode() let node1 = TreeNode() let node2 = TreeNode() node1.windowID = "0" node1.parent = rootNode node2.windowID = "1" node2.parent = rootNode rootNode.left = node1 rootNode.right = node2 expect(rootNode.treeIsValid()).to(beTrue()) rootNode.insertWindowID("2", atPoint: "0") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.left?.windowID).to(beNil()) expect(rootNode.left?.left?.windowID).to(equal("0")) expect(rootNode.left?.right?.windowID).to(equal("2")) rootNode.insertWindowID("3", atPoint: "2") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.left?.windowID).to(beNil()) expect(rootNode.left?.right?.windowID).to(beNil()) expect(rootNode.left?.right?.left?.windowID).to(equal("2")) expect(rootNode.left?.right?.right?.windowID).to(equal("3")) rootNode.insertWindowID("4", atPoint: "0") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.left?.windowID).to(beNil()) expect(rootNode.left?.left?.windowID).to(beNil()) expect(rootNode.left?.left?.left?.windowID).to(equal("0")) expect(rootNode.left?.left?.right?.windowID).to(equal("4")) } it("inserts") { let rootNode = TreeNode() let node1 = TreeNode() let node2 = TreeNode() node1.windowID = "0" node1.parent = rootNode node2.windowID = "1" node2.parent = rootNode rootNode.left = node1 rootNode.right = node2 expect(rootNode.treeIsValid()).to(beTrue()) node1.insertWindowID("2") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.left?.windowID).to(beNil()) expect(rootNode.left?.left?.windowID).to(equal("0")) expect(rootNode.left?.right?.windowID).to(equal("2")) } it("sets root value when the tree is empty") { let rootNode = TreeNode() rootNode.insertWindowIDAtEnd("0") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.windowID).to(equal("0")) } it("clears root value when inserting value after first one") { let rootNode = TreeNode() rootNode.insertWindowIDAtEnd("0") rootNode.insertWindowIDAtEnd("1") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.left).toNot(beNil()) expect(rootNode.right).toNot(beNil()) expect(rootNode.windowID).to(beNil()) } } describe("removing") { it("removes from a shallow tree") { let rootNode = TreeNode() let node1 = TreeNode() let node2 = TreeNode() let node3 = TreeNode() let node4 = TreeNode() node1.parent = rootNode node1.left = node2 node1.right = node3 node2.windowID = "0" node2.parent = node1 node3.windowID = "1" node3.parent = node1 node4.windowID = "2" node4.parent = rootNode rootNode.left = node1 rootNode.right = node4 expect(rootNode.treeIsValid()).to(beTrue()) rootNode.removeWindowID("1") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.findWindowID("1")).to(beNil()) expect(rootNode.left?.windowID).to(equal("0")) rootNode.removeWindowID("2") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.findWindowID("2")).to(beNil()) expect(rootNode.windowID).to(equal("0")) } it("removes from a deep tree") { let rootNode = TreeNode() (0..<10).forEach { rootNode.insertWindowIDAtEnd("\($0)") } expect(rootNode.treeIsValid()).to(beTrue()) rootNode.removeWindowID("5") expect(rootNode.treeIsValid()).to(beTrue()) expect(rootNode.findWindowID("5")).to(beNil()) } } describe("validitity") { it("is invalid when a node has children and an id at the same time") { let node = TreeNode() let node2 = TreeNode() let node3 = TreeNode() node.windowID = "0" node2.windowID = "1" node3.windowID = "2" expect(node.valid).to(beTrue()) node.left = node2 node.right = node3 expect(node.valid).to(beFalse()) } } it("deep compares") { let treeGenerator: () -> TreeNode = { let rootNode = TreeNode() let node1 = TreeNode() let node2 = TreeNode() let node3 = TreeNode() let node4 = TreeNode() node1.parent = rootNode node1.left = node2 node1.right = node3 node2.windowID = "0" node2.parent = node1 node3.windowID = "1" node3.parent = node1 node4.windowID = "2" node4.parent = rootNode rootNode.left = node4 rootNode.right = node1 return rootNode } let tree1 = treeGenerator() let tree2 = treeGenerator() let tree3 = treeGenerator() tree3.right?.right?.windowID = "5" expect(tree1 == tree2).to(beTrue()) expect(tree1 == tree3).to(beFalse()) } } describe("layout") { it("splits into binary partitions") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = BinarySpacePartitioningLayout() windows.forEach { layout.updateWithChange(.add(window: $0)) } let assignments = layout.frameAssignments(windowSet, on: screen)! assignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 500, height: 500), CGRect(x: 1500, y: 500, width: 500, height: 500) ]) } it("handles non-origin screens") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = BinarySpacePartitioningLayout() windows.forEach { layout.updateWithChange(.add(window: $0)) } let assignments = layout.frameAssignments(windowSet, on: screen)! assignments.verify(frames: [ CGRect(x: 100, y: 100, width: 1000, height: 1000), CGRect(x: 1100, y: 100, width: 1000, height: 500), CGRect(x: 1100, y: 600, width: 500, height: 500), CGRect(x: 1600, y: 600, width: 500, height: 500) ]) } describe("adding windows") { it("partitions the focused frame") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] var layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: $0.isFocused()) } var windowSet = WindowSet( windows: layoutWindows.dropLast(), isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = BinarySpacePartitioningLayout() windows.dropLast().forEach { layout.updateWithChange(.add(window: $0)) } var assignments = layout.frameAssignments(windowSet, on: screen)! var expectedFrames = [ CGRect(x: 0, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ] expect(assignments.frames()).to(equal(expectedFrames), description: assignments.description(withExpectedFrames: expectedFrames)) windows[1].isFocusedValue = true layoutWindows[1] = LayoutWindow(id: windows[1].id(), frame: windows[1].frame(), isFocused: windows[1].isFocused()) layout.updateWithChange(.focusChanged(window: windows[1])) layout.updateWithChange(.add(window: windows.last!)) windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) assignments = layout.frameAssignments(windowSet, on: screen)!.sorted() expectedFrames = [ CGRect(x: 0, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 500, height: 500), CGRect(x: 1500, y: 0, width: 500, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ] expect(assignments.frames()).to(equal(expectedFrames), description: assignments.description(withExpectedFrames: expectedFrames)) } it("partitions the last frame if nothing is focused") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: $0.isFocused()) } var windowSet = WindowSet( windows: layoutWindows.dropLast(), isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = BinarySpacePartitioningLayout() windows.dropLast().forEach { layout.updateWithChange(.add(window: $0)) } var assignments = layout.frameAssignments(windowSet, on: screen)! var expectedFrames = [ CGRect(x: 0, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ] expect(assignments.frames()).to(equal(expectedFrames), description: assignments.description(withExpectedFrames: expectedFrames)) layout.updateWithChange(.add(window: windows.last!)) windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) assignments = layout.frameAssignments(windowSet, on: screen)!.sorted() expectedFrames = [ CGRect(x: 0, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 500, height: 500), CGRect(x: 1500, y: 500, width: 500, height: 500) ] expect(assignments.frames()).to(equal(expectedFrames), description: assignments.description(withExpectedFrames: expectedFrames)) } } describe("removing windows") { it("expands the sibling") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = BinarySpacePartitioningLayout() windows.forEach { layout.updateWithChange(.add(window: $0)) } var assignments = layout.frameAssignments(windowSet, on: screen)! var expectedFrames = [ CGRect(x: 0, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 500, height: 500), CGRect(x: 1500, y: 500, width: 500, height: 500) ] expect(assignments.frames()).to(equal(expectedFrames), description: assignments.description(withExpectedFrames: expectedFrames)) layout.updateWithChange(.remove(window: windows[1])) assignments = layout.frameAssignments(windowSet, on: screen)! expectedFrames = [ CGRect(x: 0, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ] expect(assignments.frames()).to(equal(expectedFrames), description: assignments.description(withExpectedFrames: expectedFrames)) } } it("constructs an initial tree if necessary") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = BinarySpacePartitioningLayout() let assignments = layout.frameAssignments(windowSet, on: screen)! let expectedFrames = [ CGRect(x: 0, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 500, height: 500), CGRect(x: 1500, y: 500, width: 500, height: 500) ] expect(assignments.frames()).to(equal(expectedFrames), description: assignments.description(withExpectedFrames: expectedFrames)) } } describe("coding") { it("encodes and decodes") { let layout = BinarySpacePartitioningLayout() let window = TestWindow(element: nil)! layout.updateWithChange(.add(window: window)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(BinarySpacePartitioningLayout.self, from: encodedLayout) expect(decodedLayout).to(equal(layout)) } it("maintains the tree") { let layout = BinarySpacePartitioningLayout() let windows = [TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!] layout.updateWithChange(.add(window: windows[0])) layout.updateWithChange(.add(window: windows[1])) layout.updateWithChange(.focusChanged(window: windows[0])) layout.updateWithChange(.add(window: windows[2])) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(BinarySpacePartitioningLayout.self, from: encodedLayout) expect(decodedLayout).to(equal(layout)) let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let assignments = decodedLayout .frameAssignments(windowSet, on: screen)! .map { $0.frameAssignment } .map { [$0.window.id: $0.frame] } let expectedAssignments = layout .frameAssignments(windowSet, on: screen)! .map { $0.frameAssignment } .map { [$0.window.id: $0.frame] } expect(assignments).to(equal(expectedAssignments)) } } } } extension TreeNode { fileprivate func treeIsValid() -> Bool { var valid = self.valid if let left = left, let right = right { valid = valid && left.parent == self && right.parent == self valid = valid && left.treeIsValid() && right.treeIsValid() } return valid } } ================================================ FILE: AmethystTests/Tests/Layout/ColumnLayoutTests.swift ================================================ // // ColumnLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 9/18/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class ColumnLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("layout") { it("separates windows into columns in main pane and columns in secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ColumnLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) // The main pane is full height and the first half of the screen let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [CGRect(origin: .zero, size: CGSize(width: 1000, height: 1000))]) let secondaryFrames = secondaryAssignments.enumerated().map { index, _ in return CGRect(x: 1000.0 + 333.0 * CGFloat(index), y: 0, width: 333, height: 1000) } secondaryAssignments.verify(frames: secondaryFrames) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ColumnLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) // The main pane is full height and the first half of the screen let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [CGRect(x: 100, y: 100, width: 1000, height: 1000)]) let secondaryFrames = secondaryAssignments.enumerated().map { index, _ in return CGRect(x: 1100.0 + 333.0 * CGFloat(index), y: 100, width: 333, height: 1000) } secondaryAssignments.verify(frames: secondaryFrames) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ColumnLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 333, height: 1000), CGRect(x: 1333, y: 0, width: 333, height: 1000), CGRect(x: 1666, y: 0, width: 333, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 500, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 333, height: 1000), CGRect(x: 333, y: 0, width: 333, height: 1000), CGRect(x: 666, y: 0, width: 333, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ColumnLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1500, y: 0, width: 250, height: 1000), CGRect(x: 1750, y: 0, width: 250, height: 1000) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 750, height: 1000), CGRect(x: 1250, y: 0, width: 750, height: 1000) ]) } } describe("coding") { it("encodes and decodes") { let layout = ColumnLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(ColumnLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } } ================================================ FILE: AmethystTests/Tests/Layout/CustomLayoutTests.swift ================================================ // // CustomLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 7/8/21. // Copyright © 2021 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Cocoa import Nimble import Quick class CustomLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("undefined layout") { it("defines a name") { let layout = CustomLayout(key: "undefined", fileURL: Bundle.layoutFile(key: "undefined")!) expect(layout.layoutName).to(equal("Undefined")) } it("defines no frame assignments") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "undefined", fileURL: Bundle.layoutFile(key: "undefined")!) expect(layout.frameAssignments(windowSet, on: screen)).to(beNil()) } } describe("null layout") { it("defines a name") { let layout = CustomLayout(key: "null", fileURL: Bundle.layoutFile(key: "null")!) expect(layout.layoutName).to(equal("Null")) } it("defines no frame assignments") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "null", fileURL: Bundle.layoutFile(key: "null")!) expect(layout.frameAssignments(windowSet, on: screen)).to(beNil()) } } describe("fullscreen layout") { it("defines a name") { let layout = CustomLayout(key: "fullscreen", fileURL: Bundle.layoutFile(key: "fullscreen")!) expect(layout.layoutName).to(equal("Fullscreen")) } it("makes all windows fullscreen") { let screen = TestScreen() TestScreen.availableScreens = [screen] let windows = [TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "fullscreen", fileURL: Bundle.layoutFile(key: "fullscreen")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(frameAssignments.count).to(equal(layoutWindows.count)) frameAssignments.forEach { assignment in expect(assignment.frameAssignment.frame).to(equal(screen.adjustedFrame())) expect(assignment.frameAssignment.finalFrame).to(equal(screen.adjustedFrame())) } } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "fullscreen", fileURL: Bundle.layoutFile(key: "fullscreen")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(frameAssignments.count).to(equal(layoutWindows.count)) frameAssignments.forEach { assignment in expect(assignment.frameAssignment.frame).to(equal(screen.adjustedFrame())) expect(assignment.frameAssignment.finalFrame).to(equal(screen.adjustedFrame())) } } } describe("columns layout") { it("defines a name") { let layout = CustomLayout(key: "uniform-columns", fileURL: Bundle.layoutFile(key: "uniform-columns")!) expect(layout.layoutName).to(equal("Uniform Columns")) } it("puts windows in uniform columns") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "uniform-columns", fileURL: Bundle.layoutFile(key: "uniform-columns")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(frameAssignments.count).to(equal(layoutWindows.count)) let expectedFrames = [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 500, y: 0, width: 500, height: 1000), CGRect(x: 1000, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ] frameAssignments.verify(frames: expectedFrames) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "uniform-columns", fileURL: Bundle.layoutFile(key: "uniform-columns")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(frameAssignments.count).to(equal(layoutWindows.count)) let expectedFrames = [ CGRect(x: 100, y: 100, width: 500, height: 1000), CGRect(x: 600, y: 100, width: 500, height: 1000), CGRect(x: 1100, y: 100, width: 500, height: 1000), CGRect(x: 1600, y: 100, width: 500, height: 1000) ] frameAssignments.verify(frames: expectedFrames) } } describe("static ratio tall layout") { it("defines a name") { let layout = CustomLayout(key: "static-ratio-tall", fileURL: Bundle.layoutFile(key: "static-ratio-tall")!) expect(layout.layoutName).to(equal("Static Ratio Tall")) } it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "static-ratio-tall", fileURL: Bundle.layoutFile(key: "static-ratio-tall")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(origin: .zero, size: CGSize(width: 1000, height: 1000)) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "static-ratio-tall", fileURL: Bundle.layoutFile(key: "static-ratio-tall")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1100, y: 100, width: 1000, height: 500), CGRect(x: 1100, y: 600, width: 1000, height: 500) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "static-ratio-tall", fileURL: Bundle.layoutFile(key: "static-ratio-tall")!) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 333), CGRect(x: 1000, y: 333, width: 1000, height: 333), CGRect(x: 1000, y: 666, width: 1000, height: 333) ]) layout.command3() frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) layout.command3() frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 333), CGRect(x: 0, y: 333, width: 1000, height: 333), CGRect(x: 0, y: 666, width: 1000, height: 333) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) } } describe("static ratio tall layout with proxied native commands") { it("defines a name") { let layout = CustomLayout(key: "static-ratio-tall-native-commands", fileURL: Bundle.layoutFile(key: "static-ratio-tall-native-commands")!) expect(layout.layoutName).to(equal("Static Ratio Tall with Native Commands")) } it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "static-ratio-tall-native-commands", fileURL: Bundle.layoutFile(key: "static-ratio-tall-native-commands")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(origin: .zero, size: CGSize(width: 1000, height: 1000)) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "static-ratio-tall-native-commands", fileURL: Bundle.layoutFile(key: "static-ratio-tall-native-commands")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1100, y: 100, width: 1000, height: 500), CGRect(x: 1100, y: 600, width: 1000, height: 500) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "static-ratio-tall-native-commands", fileURL: Bundle.layoutFile(key: "static-ratio-tall-native-commands")!) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 333), CGRect(x: 1000, y: 333, width: 1000, height: 333), CGRect(x: 1000, y: 666, width: 1000, height: 333) ]) layout.increaseMainPaneCount() frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) layout.increaseMainPaneCount() frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 333), CGRect(x: 0, y: 333, width: 1000, height: 333), CGRect(x: 0, y: 666, width: 1000, height: 333) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) } } describe("subset layout") { it("defines a name") { let layout = CustomLayout(key: "subset", fileURL: Bundle.layoutFile(key: "subset")!) expect(layout.layoutName).to(equal("Subset")) } describe("subset") { it("starts empty") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "subset", fileURL: Bundle.layoutFile(key: "subset")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 250), CGRect(x: 1000, y: 250, width: 1000, height: 250), CGRect(x: 1000, y: 500, width: 1000, height: 250), CGRect(x: 1000, y: 750, width: 1000, height: 250) ], inOrder: false) } it("adds and removes ids to the subset") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "subset", fileURL: Bundle.layoutFile(key: "subset")!) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 250), CGRect(x: 1000, y: 250, width: 1000, height: 250), CGRect(x: 1000, y: 500, width: 1000, height: 250), CGRect(x: 1000, y: 750, width: 1000, height: 250) ]) TestWindow.focused = windows[0] layout.command3() TestWindow.focused = windows[3] layout.command3() frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) TestWindow.focused = windows[0] layout.command4() frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 333), CGRect(x: 1000, y: 333, width: 1000, height: 333), CGRect(x: 1000, y: 666, width: 1000, height: 333), CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) TestWindow.focused = windows[3] layout.command4() frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 250), CGRect(x: 1000, y: 250, width: 1000, height: 250), CGRect(x: 1000, y: 500, width: 1000, height: 250), CGRect(x: 1000, y: 750, width: 1000, height: 250) ]) } it("swaps subset state on window swaps") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "subset", fileURL: Bundle.layoutFile(key: "subset")!) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ windows[0].id(): CGRect(x: 1000, y: 0, width: 1000, height: 250), windows[1].id(): CGRect(x: 1000, y: 250, width: 1000, height: 250), windows[2].id(): CGRect(x: 1000, y: 500, width: 1000, height: 250), windows[3].id(): CGRect(x: 1000, y: 750, width: 1000, height: 250) ]) TestWindow.focused = windows[0] layout.command3() TestWindow.focused = windows[3] layout.command3() frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ windows[0].id(): CGRect(x: 0, y: 0, width: 1000, height: 500), windows[1].id(): CGRect(x: 1000, y: 0, width: 1000, height: 500), windows[2].id(): CGRect(x: 1000, y: 500, width: 1000, height: 500), windows[3].id(): CGRect(x: 0, y: 500, width: 1000, height: 500) ]) layout.updateWithChange(.windowSwap(window: windows[1], otherWindow: windows[0])) frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ windows[0].id(): CGRect(x: 1000, y: 0, width: 1000, height: 500), windows[1].id(): CGRect(x: 0, y: 0, width: 1000, height: 500), windows[2].id(): CGRect(x: 1000, y: 500, width: 1000, height: 500), windows[3].id(): CGRect(x: 0, y: 500, width: 1000, height: 500) ]) layout.updateWithChange(.windowSwap(window: windows[3], otherWindow: windows[2])) frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.verify(frames: [ windows[0].id(): CGRect(x: 1000, y: 0, width: 1000, height: 500), windows[1].id(): CGRect(x: 0, y: 0, width: 1000, height: 500), windows[2].id(): CGRect(x: 0, y: 500, width: 1000, height: 500), windows[3].id(): CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) } } } describe("extended") { describe("tall right") { it("swaps columns") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = CustomLayout(key: "extended", fileURL: Bundle.layoutFile(key: "extended")!) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) } } } describe("recommend main pane ratio") { it("receives correct ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let window = TestWindow(element: nil)! let layoutWindow = LayoutWindow(id: window.id(), frame: window.frame(), isFocused: false) let windowSet = WindowSet( windows: [layoutWindow], isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { _ in window } ) let layout = CustomLayout(key: "recommended-main-pane-ratio", fileURL: Bundle.layoutFile(key: "recommended-main-pane-ratio")!) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignment = frameAssignments.forWindows([window]) mainAssignment.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) layout.recommendMainPaneRawRatio(rawRatio: 0.25) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignment = frameAssignments.forWindows([window]) mainAssignment.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000) ]) layout.recommendMainPaneRawRatio(rawRatio: 0.75) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignment = frameAssignments.forWindows([window]) mainAssignment.verify(frames: [ CGRect(x: 0, y: 0, width: 1500, height: 1000) ]) } } } } ================================================ FILE: AmethystTests/Tests/Layout/FloatingLayoutTests.swift ================================================ // // FloatingLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 9/21/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class FloatingLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("layout") { it("generates no assignments") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = FloatingLayout() expect(layout.frameAssignments(windowSet, on: screen)).to(beNil()) } } describe("coding") { it("encodes and decodes") { let layout = FloatingLayout() let encodedLayout = try! JSONEncoder().encode(layout) expect { try JSONDecoder().decode(FloatingLayout.self, from: encodedLayout) }.toNot(throwError()) } } } } ================================================ FILE: AmethystTests/Tests/Layout/FullscreenLayoutTests.swift ================================================ // // FullscreenLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 9/14/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class FullscreenLayoutTests: QuickSpec { private lazy var operationQueue: OperationQueue = { let operationQueue = OperationQueue() operationQueue.maxConcurrentOperationCount = 1 return operationQueue }() override func spec() { afterEach { TestScreen.availableScreens = [] } describe("layout") { it("makes all windows fullscreen") { let screen = TestScreen() TestScreen.availableScreens = [screen] let windows = [TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = FullscreenLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.forEach { assignment in expect(assignment.frameAssignment.frame).to(equal(screen.adjustedFrame())) expect(assignment.frameAssignment.finalFrame).to(equal(screen.adjustedFrame())) } } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = FullscreenLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! frameAssignments.forEach { assignment in expect(assignment.frameAssignment.frame).to(equal(screen.adjustedFrame())) expect(assignment.frameAssignment.finalFrame).to(equal(screen.adjustedFrame())) } } } describe("coding") { it("encodes and decodes") { let layout = FullscreenLayout() let encodedLayout = try! JSONEncoder().encode(layout) expect { try JSONDecoder().decode(FullscreenLayout.self, from: encodedLayout) }.toNot(throwError()) } } } } ================================================ FILE: AmethystTests/Tests/Layout/RowLayoutTests.swift ================================================ // // RowLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 9/21/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class RowLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("layout") { it("separates windows into rows in main pane and rows in secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = RowLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) // The main pane is full width and the top half of the screen let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [CGRect(origin: .zero, size: CGSize(width: 2000, height: 500))]) let secondaryFrames = secondaryAssignments.enumerated().map { index, _ in return CGRect(x: 0, y: 500.0 + 166 * CGFloat(index), width: 2000, height: 166) } secondaryAssignments.verify(frames: secondaryFrames) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = RowLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) // The main pane is full width and the top half of the screen let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [CGRect(x: 100, y: 100, width: 2000, height: 500)]) let secondaryFrames = secondaryAssignments.enumerated().map { index, _ in return CGRect(x: 100, y: 600.0 + 166 * CGFloat(index), width: 2000, height: 166) } secondaryAssignments.verify(frames: secondaryFrames) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = RowLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 2000, height: 166), CGRect(x: 0, y: 666, width: 2000, height: 166), CGRect(x: 0, y: 832, width: 2000, height: 166) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 250), CGRect(x: 0, y: 250, width: 2000, height: 250) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 2000, height: 250), CGRect(x: 0, y: 750, width: 2000, height: 250) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 166), CGRect(x: 0, y: 166, width: 2000, height: 166), CGRect(x: 0, y: 332, width: 2000, height: 166) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 498, width: 2000, height: 500) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = RowLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 2000, height: 166), CGRect(x: 0, y: 666, width: 2000, height: 166), CGRect(x: 0, y: 832, width: 2000, height: 166) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 750) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 750, width: 2000, height: 83), CGRect(x: 0, y: 833, width: 2000, height: 83), CGRect(x: 0, y: 916, width: 2000, height: 83) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 250) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 250, width: 2000, height: 250), CGRect(x: 0, y: 500, width: 2000, height: 250), CGRect(x: 0, y: 750, width: 2000, height: 250) ]) } } describe("coding") { it("encodes and decodes") { let layout = RowLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(RowLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } } ================================================ FILE: AmethystTests/Tests/Layout/TallLayoutTests.swift ================================================ // // TallLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 9/21/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class TallLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("layout") { it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TallLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(origin: .zero, size: CGSize(width: 1000, height: 1000)) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TallLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1100, y: 100, width: 1000, height: 500), CGRect(x: 1100, y: 600, width: 1000, height: 500) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TallLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 333), CGRect(x: 1000, y: 333, width: 1000, height: 333), CGRect(x: 1000, y: 666, width: 1000, height: 333) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 333), CGRect(x: 0, y: 333, width: 1000, height: 333), CGRect(x: 0, y: 666, width: 1000, height: 333) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TallLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1500, y: 0, width: 500, height: 500), CGRect(x: 1500, y: 500, width: 500, height: 500) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1500, height: 500), CGRect(x: 500, y: 500, width: 1500, height: 500) ]) } } describe("coding") { it("encodes and decodes") { let layout = TallLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(TallLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } } ================================================ FILE: AmethystTests/Tests/Layout/TallRightLayoutTests.swift ================================================ // // TallRightLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 12/18/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class TallRightLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("layout") { it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TallRightLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 1000, height: 500), CGRect(x: 0, y: 0, width: 1000, height: 500) ]) } it("handles non-origin screens") { let screen = TestScreen(frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TallRightLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 1100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 100, y: 600, width: 1000, height: 500), CGRect(x: 100, y: 100, width: 1000, height: 500) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TallRightLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 666, width: 1000, height: 333), CGRect(x: 0, y: 333, width: 1000, height: 333), CGRect(x: 0, y: 0, width: 1000, height: 333) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 1000, height: 500), CGRect(x: 0, y: 0, width: 1000, height: 500) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 333), CGRect(x: 1000, y: 333, width: 1000, height: 333), CGRect(x: 1000, y: 666, width: 1000, height: 333) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TallRightLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 1000, height: 500), CGRect(x: 0, y: 0, width: 1000, height: 500) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 500, height: 500), CGRect(x: 0, y: 0, width: 500, height: 500) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 1500, height: 500), CGRect(x: 0, y: 0, width: 1500, height: 500) ]) } } describe("coding") { it("encodes and decodes") { let layout = TallRightLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(TallRightLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } } ================================================ FILE: AmethystTests/Tests/Layout/ThreeColumnLayoutTests.swift ================================================ // // ThreeColumnLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 12/19/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class ThreeColumnLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("TriplePaneArrangement") { describe("pane counts") { it("takes windows in main pane up to provided count") { let mainPaneCount: UInt = 2 let screenSize = CGSize(width: 2000, height: 1000) let count: (UInt) -> UInt = { windowCount -> UInt in return TriplePaneArrangement( mainPane: .left, numWindows: windowCount, numMainPane: mainPaneCount, screenSize: screenSize, mainPaneRatio: 0.5 ).count(.main) } expect(count(1)).to(equal(1)) expect(count(2)).to(equal(2)) expect(count(4)).to(equal(2)) } it("splits non-main windows between two panes") { let mainPaneCount: UInt = 0 let screenSize = CGSize(width: 2000, height: 1000) let secondaryCount: (UInt) -> UInt = { windowCount -> UInt in return TriplePaneArrangement( mainPane: .left, numWindows: windowCount, numMainPane: mainPaneCount, screenSize: screenSize, mainPaneRatio: 0.5 ).count(.secondary) } let tertiaryCount: (UInt) -> UInt = { windowCount -> UInt in return TriplePaneArrangement( mainPane: .left, numWindows: windowCount, numMainPane: mainPaneCount, screenSize: screenSize, mainPaneRatio: 0.5 ).count(.tertiary) } expect(secondaryCount(1)).to(equal(1)) expect(secondaryCount(2)).to(equal(1)) expect(secondaryCount(3)).to(equal(2)) expect(secondaryCount(4)).to(equal(2)) expect(tertiaryCount(1)).to(equal(0)) expect(tertiaryCount(2)).to(equal(1)) expect(tertiaryCount(3)).to(equal(1)) expect(tertiaryCount(4)).to(equal(2)) } } it("splits panes into rows") { let mainPaneCount: UInt = 2 let screenSize = CGSize(width: 2000, height: 1000) let height: (UInt, Pane) -> CGFloat = { windowCount, pane -> CGFloat in return TriplePaneArrangement( mainPane: .left, numWindows: windowCount, numMainPane: mainPaneCount, screenSize: screenSize, mainPaneRatio: 0.5 ).height(pane) } expect(height(1, .main)).to(equal(1000)) expect(height(2, .main)).to(equal(500)) expect(height(3, .main)).to(equal(500)) expect(height(1, .secondary)).to(equal(0)) expect(height(2, .secondary)).to(equal(0)) expect(height(3, .secondary)).to(equal(1000)) expect(height(4, .secondary)).to(equal(1000)) expect(height(5, .secondary)).to(equal(500)) expect(height(6, .secondary)).to(equal(500)) expect(height(1, .tertiary)).to(equal(0)) expect(height(2, .tertiary)).to(equal(0)) expect(height(3, .tertiary)).to(equal(0)) expect(height(4, .tertiary)).to(equal(1000)) expect(height(5, .tertiary)).to(equal(1000)) expect(height(6, .tertiary)).to(equal(500)) } } describe("middle layout") { it("separates into a main pane and two secondary panes") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnMiddleLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 500, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) } it("handles non-origin screens") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnMiddleLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 600, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 100, y: 100, width: 500, height: 1000), CGRect(x: 1600, y: 100, width: 500, height: 1000) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnMiddleLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 500), CGRect(x: 0, y: 500, width: 500, height: 500), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1000, height: 500), CGRect(x: 500, y: 500, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1000, height: 333), CGRect(x: 500, y: 333, width: 1000, height: 333), CGRect(x: 500, y: 666, width: 1000, height: 333) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnMiddleLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 250, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 250, height: 1000), CGRect(x: 1750, y: 0, width: 250, height: 1000) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 750, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 750, height: 1000), CGRect(x: 1250, y: 0, width: 750, height: 1000) ]) } describe("coding") { it("encodes and decodes") { let layout = ThreeColumnMiddleLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(ThreeColumnMiddleLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } describe("left layout") { it("separates into a main pane and two secondary panes") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnLeftLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnLeftLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1100, y: 100, width: 500, height: 1000), CGRect(x: 1600, y: 100, width: 500, height: 1000) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnLeftLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 500, height: 500), CGRect(x: 1000, y: 500, width: 500, height: 500), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 333), CGRect(x: 0, y: 333, width: 1000, height: 333), CGRect(x: 0, y: 666, width: 1000, height: 333) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 500, height: 1000) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnLeftLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1500, y: 0, width: 250, height: 1000), CGRect(x: 1750, y: 0, width: 250, height: 1000) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 750, height: 1000), CGRect(x: 1250, y: 0, width: 750, height: 1000) ]) } describe("coding") { it("encodes and decodes") { let layout = ThreeColumnLeftLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(ThreeColumnLeftLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } describe("right layout") { it("separates into a main pane and two secondary panes") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnRightLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 500, y: 0, width: 500, height: 1000) ]) } it("separates into a main pane and two secondary panes") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnRightLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 1100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 100, y: 100, width: 500, height: 1000), CGRect(x: 600, y: 100, width: 500, height: 1000) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnRightLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 500), CGRect(x: 0, y: 500, width: 500, height: 500), CGRect(x: 500, y: 0, width: 500, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 500, y: 0, width: 500, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 333), CGRect(x: 1000, y: 333, width: 1000, height: 333), CGRect(x: 1000, y: 666, width: 1000, height: 333) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = ThreeColumnRightLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 500, y: 0, width: 500, height: 1000) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 250, height: 1000), CGRect(x: 250, y: 0, width: 250, height: 1000) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 750, height: 1000), CGRect(x: 750, y: 0, width: 750, height: 1000) ]) } describe("coding") { it("encodes and decodes") { let layout = ThreeColumnRightLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(ThreeColumnRightLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } } } ================================================ FILE: AmethystTests/Tests/Layout/TwoPaneLayoutTests.swift ================================================ // // TwoPaneLayoutTests.swift // AmethystTests // // Created by @mwz on 14/06/21. // Copyright © 2021 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class TwoPaneLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("layout horizontal") { it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TwoPaneLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(origin: .zero, size: CGSize(width: 1000, height: 1000)) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TwoPaneLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1100, y: 100, width: 1000, height: 1000), CGRect(x: 1100, y: 100, width: 1000, height: 1000) ]) } it("does not increase and decrease windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TwoPaneLayout() expect(layout.mainPaneCount).to(equal(1)) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! let mainAssignments = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(1)) layout.decreaseMainPaneCount() expect(layout.mainPaneCount).to(equal(1)) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TwoPaneLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000), CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1500, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1500, height: 1000), CGRect(x: 500, y: 0, width: 1500, height: 1000) ]) } } describe("layout vertical") { it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 1000, height: 2000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TwoPaneLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(origin: .zero, size: CGSize(width: 1000, height: 1000)) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 1000, width: 1000, height: 1000), CGRect(x: 0, y: 1000, width: 1000, height: 1000) ]) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 1000, height: 2000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TwoPaneLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 100, y: 1100, width: 1000, height: 1000), CGRect(x: 100, y: 1100, width: 1000, height: 1000) ]) } it("does not increase and decrease windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 1000, height: 2000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TwoPaneLayout() expect(layout.mainPaneCount).to(equal(1)) let frameAssignments = layout.frameAssignments(windowSet, on: screen)! let mainAssignments = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 1000, width: 1000, height: 1000), CGRect(x: 0, y: 1000, width: 1000, height: 1000), CGRect(x: 0, y: 1000, width: 1000, height: 1000) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(1)) layout.decreaseMainPaneCount() expect(layout.mainPaneCount).to(equal(1)) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 1000, height: 2000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = TwoPaneLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 1000, width: 1000, height: 1000), CGRect(x: 0, y: 1000, width: 1000, height: 1000) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 1500, width: 1000, height: 500), CGRect(x: 0, y: 1500, width: 1000, height: 500) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 1000, height: 1500), CGRect(x: 0, y: 500, width: 1000, height: 1500) ]) } } describe("coding") { it("encodes and decodes") { let layout = TwoPaneLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(1)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(TwoPaneLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(1)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } } ================================================ FILE: AmethystTests/Tests/Layout/WideLayoutTests.swift ================================================ // // WideLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 12/7/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class WideLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("layout") { it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WideLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(origin: .zero, size: CGSize(width: 2000, height: 500)) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WideLayout() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 100, y: 100, width: 2000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 100, y: 600, width: 1000, height: 500), CGRect(x: 1100, y: 600, width: 1000, height: 500) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WideLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 667, height: 500), CGRect(x: 667, y: 500, width: 667, height: 500), CGRect(x: 1334, y: 500, width: 667, height: 500) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 0, width: 1000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 667, height: 500), CGRect(x: 667, y: 0, width: 667, height: 500), CGRect(x: 1334, y: 0, width: 667, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 2000, height: 500) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WideLayout() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 500) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 500, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 750) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 750, width: 1000, height: 250), CGRect(x: 1000, y: 750, width: 1000, height: 250) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 2000, height: 250) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 250, width: 1000, height: 750), CGRect(x: 1000, y: 250, width: 1000, height: 750) ]) } } describe("coding") { it("encodes and decodes") { let layout = WideLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(WideLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } } ================================================ FILE: AmethystTests/Tests/Layout/WidescreenTallLayoutTests.swift ================================================ // // WidescreenTallLayoutTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 12/18/19. // Copyright © 2019 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class WidescreenTallLayoutTests: QuickSpec { override func spec() { afterEach { TestScreen.availableScreens = [] } describe("left layout") { it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WidescreenTallLayoutLeft() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(origin: .zero, size: CGSize(width: 1000, height: 1000)) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) } it("handles non-origin screen") { let screen = TestScreen(frame: CGRect(x: 100, y: 100, width: 2000, height: 1000)) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WidescreenTallLayoutLeft() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 100, y: 100, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1100, y: 100, width: 1000, height: 500), CGRect(x: 1100, y: 600, width: 1000, height: 500) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WidescreenTallLayoutLeft() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 333), CGRect(x: 1000, y: 333, width: 1000, height: 333), CGRect(x: 1000, y: 666, width: 1000, height: 333) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000), CGRect(x: 500, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 333, height: 1000), CGRect(x: 333, y: 0, width: 333, height: 1000), CGRect(x: 666, y: 0, width: 333, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WidescreenTallLayoutLeft() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 500), CGRect(x: 1000, y: 500, width: 1000, height: 500) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 1500, y: 0, width: 500, height: 500), CGRect(x: 1500, y: 500, width: 500, height: 500) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1500, height: 500), CGRect(x: 500, y: 500, width: 1500, height: 500) ]) } } describe("right layout") { it("separates into a main pane and a secondary pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WidescreenTallLayoutRight() let frameAssignments = layout.frameAssignments(windowSet, on: screen)! expect(layout.mainPaneCount).to(equal(1)) let mainAssignment = frameAssignments.forWindows(windows[..<1]) let secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignment.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) } it("increases and decreases windows in the main pane") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WidescreenTallLayoutRight() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 333), CGRect(x: 0, y: 333, width: 1000, height: 333), CGRect(x: 0, y: 666, width: 1000, height: 333) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(2)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<2]) secondaryAssignments = frameAssignments.forWindows(windows[2...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 500, height: 1000), CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) layout.increaseMainPaneCount() expect(layout.mainPaneCount).to(equal(3)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<3]) secondaryAssignments = frameAssignments.forWindows(windows[3...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 333, height: 1000), CGRect(x: 1333, y: 0, width: 333, height: 1000), CGRect(x: 1666, y: 0, width: 333, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 1000) ]) } it("changes distribution based on pane ratio") { let screen = TestScreen(frame: CGRect(origin: .zero, size: CGSize(width: 2000, height: 1000))) TestScreen.availableScreens = [screen] let windows = [ TestWindow(element: nil)!, TestWindow(element: nil)!, TestWindow(element: nil)! ] let layoutWindows = windows.map { LayoutWindow(id: $0.id(), frame: $0.frame(), isFocused: false) } let windowSet = WindowSet( windows: layoutWindows, isWindowWithIDActive: { _ in return true }, isWindowWithIDFloating: { _ in return false }, windowForID: { id in return windows.first { $0.id() == id } } ) let layout = WidescreenTallLayoutRight() expect(layout.mainPaneCount).to(equal(1)) var frameAssignments = layout.frameAssignments(windowSet, on: screen)! var mainAssignments = frameAssignments.forWindows(windows[..<1]) var secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1000, y: 0, width: 1000, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1000, height: 500), CGRect(x: 0, y: 500, width: 1000, height: 500) ]) layout.recommendMainPaneRatio(0.75) expect(layout.mainPaneRatio).to(equal(0.75)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 500, y: 0, width: 1500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 500, height: 500), CGRect(x: 0, y: 500, width: 500, height: 500) ]) layout.recommendMainPaneRatio(0.25) expect(layout.mainPaneRatio).to(equal(0.25)) frameAssignments = layout.frameAssignments(windowSet, on: screen)! mainAssignments = frameAssignments.forWindows(windows[..<1]) secondaryAssignments = frameAssignments.forWindows(windows[1...]) mainAssignments.verify(frames: [ CGRect(x: 1500, y: 0, width: 500, height: 1000) ]) secondaryAssignments.verify(frames: [ CGRect(x: 0, y: 0, width: 1500, height: 500), CGRect(x: 0, y: 500, width: 1500, height: 500) ]) } } describe("coding") { it("encodes and decodes") { let layout = WidescreenTallLayout() layout.increaseMainPaneCount() layout.recommendMainPaneRatio(0.45) expect(layout.mainPaneCount).to(equal(2)) expect(layout.mainPaneRatio).to(equal(0.45)) let encodedLayout = try! JSONEncoder().encode(layout) let decodedLayout = try! JSONDecoder().decode(WidescreenTallLayout.self, from: encodedLayout) expect(decodedLayout.mainPaneCount).to(equal(2)) expect(decodedLayout.mainPaneRatio).to(equal(0.45)) } } } } ================================================ FILE: AmethystTests/Tests/Managers/HotKeyManagerTests.swift ================================================ // // HotKeyManagerTests.swift // Amethyst // // Created by Ian Ynda-Hummel on 4/18/17. // Copyright © 2017 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica class HotKeyManagerTests: QuickSpec { override func spec() { describe("hotKeyNameToDefaultsKey") { it("has the right number of screens") { let keyMapping = HotKeyManager.hotKeyNameToDefaultsKey() let screenCommands = keyMapping.filter { $0[1].hasPrefix(CommandKey.focusScreenPrefix.rawValue) } expect(screenCommands.count).to(equal(7)) } } } } ================================================ FILE: AmethystTests/Tests/Managers/ScreenManagerTests.swift ================================================ // // ScreenManagerTests.swift // AmethystTests // // Created by Ian Ynda-Hummel on 2/11/20. // Copyright © 2020 Ian Ynda-Hummel. All rights reserved. // @testable import Amethyst import Nimble import Quick import Silica private final class TestDelegate: ScreenManagerDelegate { typealias Window = TestWindow func applyWindowLimit(forScreenManager screenManager: ScreenManager, minimizingIn range: (Int) -> Range) { fatalError() } func activeWindowSet(forScreenManager screenManager: ScreenManager) -> WindowSet { fatalError() } func onReflowInitiation() { fatalError() } func onReflowCompletion() { fatalError() } } class ScreenManagerTests: QuickSpec { override func spec() { describe("coding") { it("decodes layouts") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.setLayoutKeys(LayoutType.standardLayoutClasses().map { $0.layoutKey }) let layouts = LayoutType.standardLayoutClasses().map { $0.init() } let encoder = JSONEncoder() let encodedLayouts = layouts.map { ["key": $0.layoutKey.data(using: .utf8)!, "data": try! encoder.encode($0)] } let decodedLayouts = try! ScreenManager.decodedLayouts(from: encodedLayouts, userConfiguration: configuration) expect(decodedLayouts.count).to(equal(layouts.count)) expect(decodedLayouts.count).to(equal(encodedLayouts.count)) expect(decodedLayouts.map { $0.layoutKey }).to(equal(layouts.map { $0.layoutKey })) } it("replaces incorrectly encoded layouts") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.setLayoutKeys([FullscreenLayout.layoutKey, TallLayout.layoutKey]) let encoder = JSONEncoder() let encodedLayouts = [ ["key": FullscreenLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(FullscreenLayout())], ["key": TallLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(["incorrect": "encoding"])] ] let decodedLayouts = try! ScreenManager.decodedLayouts(from: encodedLayouts, userConfiguration: configuration) expect { try JSONDecoder().decode(TallLayout.self, from: encodedLayouts[1]["data"]!) }.to(throwError()) expect(decodedLayouts.count).to(equal(2)) expect(decodedLayouts.map { $0.layoutKey }).to(equal(configuration.layoutKeys())) } it("replaces incorrectly keyed layouts") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.setLayoutKeys([FullscreenLayout.layoutKey, TallLayout.layoutKey]) let encoder = JSONEncoder() let encodedLayouts = [ ["key": FullscreenLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(FullscreenLayout())], ["key": FullscreenLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(TallLayout())] ] let decodedLayouts = try! ScreenManager.decodedLayouts(from: encodedLayouts, userConfiguration: configuration) expect(decodedLayouts.count).to(equal(2)) expect(decodedLayouts.map { $0.layoutKey }).to(equal(configuration.layoutKeys())) } context("layout list changes") { it("maintains encoded layouts on insertions") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.setLayoutKeys([ FullscreenLayout.layoutKey, WideLayout.layoutKey, TallLayout.layoutKey ]) let encoder = JSONEncoder() let tallLayout = TallLayout() tallLayout.increaseMainPaneCount() expect(tallLayout.mainPaneCount).to(equal(2)) let encodedLayouts = [ ["key": FullscreenLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(FullscreenLayout())], ["key": TallLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(tallLayout)] ] let decodedLayouts = try! ScreenManager.decodedLayouts(from: encodedLayouts, userConfiguration: configuration) expect(decodedLayouts.count).to(equal(3)) expect(decodedLayouts.map { $0.layoutKey }).to(equal(configuration.layoutKeys())) expect((decodedLayouts.last as? TallLayout)?.mainPaneCount).to(equal(2)) } it("maintains encoded layouts on appends") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.setLayoutKeys([ FullscreenLayout.layoutKey, TallLayout.layoutKey, WideLayout.layoutKey ]) let encoder = JSONEncoder() let tallLayout = TallLayout() tallLayout.increaseMainPaneCount() expect(tallLayout.mainPaneCount).to(equal(2)) let encodedLayouts = [ ["key": FullscreenLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(FullscreenLayout())], ["key": TallLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(tallLayout)] ] let decodedLayouts = try! ScreenManager.decodedLayouts(from: encodedLayouts, userConfiguration: configuration) expect(decodedLayouts.count).to(equal(3)) expect(decodedLayouts.map { $0.layoutKey }).to(equal(configuration.layoutKeys())) expect((decodedLayouts[1] as? TallLayout)?.mainPaneCount).to(equal(2)) } it("maintains encoded layouts on prepends") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.setLayoutKeys([ WideLayout.layoutKey, FullscreenLayout.layoutKey, TallLayout.layoutKey ]) let encoder = JSONEncoder() let tallLayout = TallLayout() tallLayout.increaseMainPaneCount() expect(tallLayout.mainPaneCount).to(equal(2)) let encodedLayouts = [ ["key": FullscreenLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(FullscreenLayout())], ["key": TallLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(tallLayout)] ] let decodedLayouts = try! ScreenManager.decodedLayouts(from: encodedLayouts, userConfiguration: configuration) expect(decodedLayouts.count).to(equal(3)) expect(decodedLayouts.map { $0.layoutKey }).to(equal(configuration.layoutKeys())) expect((decodedLayouts.last as? TallLayout)?.mainPaneCount).to(equal(2)) } it("maintains existing layouts on deletes") { let configuration = UserConfiguration(storage: TestConfigurationStorage()) configuration.setLayoutKeys([ WideLayout.layoutKey, TallLayout.layoutKey ]) let encoder = JSONEncoder() let wideLayout = WideLayout() let tallLayout = TallLayout() wideLayout.increaseMainPaneCount() tallLayout.increaseMainPaneCount() expect(wideLayout.mainPaneCount).to(equal(2)) expect(tallLayout.mainPaneCount).to(equal(2)) let encodedLayouts = [ ["key": WideLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(wideLayout)], ["key": FullscreenLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(FullscreenLayout())], ["key": TallLayout.layoutKey.data(using: .utf8)!, "data": try! encoder.encode(tallLayout)] ] let decodedLayouts = try! ScreenManager.decodedLayouts(from: encodedLayouts, userConfiguration: configuration) expect(decodedLayouts.count).to(equal(2)) expect(decodedLayouts.map { $0.layoutKey }).to(equal(configuration.layoutKeys())) expect((decodedLayouts.first as? WideLayout)?.mainPaneCount).to(equal(2)) expect((decodedLayouts.last as? TallLayout)?.mainPaneCount).to(equal(2)) } } } } } ================================================ FILE: Brewfile ================================================ # run 'brew bundle' to install all listed packages # Build tools brew "fastlane" # Easiest way to build and release mobile apps brew "xcbeautify" # Little beautifier tool for xcodebuild brew "swiftlint" ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ianynda@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2015 Ian Ynda-Hummel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Amethyst [![Discussions](https://img.shields.io/github/discussions/ianyh/Amethyst)](https://github.com/ianyh/Amethyst/discussions) [![Open Source Helpers](https://www.codetriage.com/ianyh/amethyst/badges/users.svg)](https://www.codetriage.com/ianyh/amethyst) [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) Tiling window manager for macOS along the lines of [xmonad](https://xmonad.org/). ![Windows](https://ianyh.com/amethyst/images/windows.png) If you want to learn more about tiling window managers and the features of Amethyst there are some great community resources on YouTube. [Boost your MacOS PRODUCTIVITY with Amethyst | Tiling Window Manager](https://www.youtube.com/watch?v=7Z9-Ry4yGNc) ## Getting Amethyst Amethyst is available for direct download on the [releases page](https://github.com/ianyh/Amethyst/releases) or using [homebrew cask](https://github.com/Homebrew/homebrew-cask). ``` brew install --cask amethyst ``` Note: that Amethyst now is only supported on macOS 10.15+. ## Using Amethyst Amethyst must be given permissions to use the accessibility APIs in the Privacy & Security tab, Privacy -> Accessibilty.

Give Accessibility permission to Amethyst under Privicay and Security.

**_Important note_**: You will probably want to disable `Automatically rearrange Spaces based on most recent use` (found under Mission Control in System Preferences). This setting is enabled by default, and will cause your Spaces to swap places based on use. This makes keyboard navigation between Spaces unpredictable.

or run in a terminal: ```bash defaults write com.apple.dock workspaces-auto-swoosh -bool NO killall Dock ``` ## Troubleshooting See [Troubleshooting](docs/troubleshooting.md) for some common issues. ## Configuration ### Keyboard Shortcuts Amethyst uses two modifier combinations (`mod1` and `mod2`) and can optionally use another two (`mod3` and `mod4`). | Default Shortcut | Description | |---|---| | `mod1` | `option + shift` | | `mod2` | `ctrl + option + shift` | | `mod3` | not defined by default | | `mod4` | not defined by default | And defines the following commands, mostly a mapping to xmonad key combinations. | Default Shortcut | Description | |---|---| | `mod1 + space` | Cycle layout forward | | `mod2 + space` | Cycle layout backwards | | `mod1 + h` | Shrink the main pane | | `mod1 + l` | Expand the main pane | | `mod1 + ,` | Increase main pane count | | `mod1 + .` | Decrease main pane count | | `mod1 + j` | Move focus counter clockwise | | `mod1 + k` | Move focus clockwise | | `mod1 + p` | Move focus to counter clockwise screen | | `mod1 + n` | Move focus to clockwise screen | | `mod2 + h` | Swap focused window to counter clockwise screen | | `mod2 + l` | Swap focused window to clockwise screen | | `mod2 + j` | Swap focused window counter clockwise | | `mod2 + k` | Swap focused window clockwise | | `mod1 + enter` | Swap focused window with main window | | `mod1 + z` | Force windows to be reevaluated | | `mod2 + z` | Relaunch Amethyst | | `mod2 + left` | Throw focused window to space left | | `mod2 + right` | Throw focused window to space right | | `mod2 + 1` | Throw focused window to space 1 | | `mod2 + 2` | Throw focused window to space 2 | | `mod2 + 3` | Throw focused window to space 3 | | `mod2 + 4` | Throw focused window to space 4 | | `mod2 + 5` | Throw focused window to space 5 | | `mod2 + 6` | Throw focused window to space 6 | | `mod2 + 7` | Throw focused window to space 7 | | `mod2 + 8` | Throw focused window to space 8 | | `mod2 + 9` | Throw focused window to space 9 | | `mod2 + 0` | Throw focused window to space 10 | | `none` | Throw focused window to space 11 | | `none` | Throw focused window to space 12 | | `none` | Throw focused window to space 13 | | `none` | Throw focused window to space 14 | | `none` | Throw focused window to space 15 | | `none` | Throw focused window to space 16 | | `mod1 + w` | Focus Screen 1 | | `mod2 + w` | Throw focused window to screen 1 | | `mod1 + e` | Focus Screen 2 | | `mod2 + e` | Throw focused window to screen 2 | | `mod1 + r` | Focus Screen 3 | | `mod2 + r` | Throw focused window to screen 3 | | `mod1 + q` | Focus Screen 4 | | `mod2 + q` | Throw focused window to screen 4 | | `mod1 + g` | Focus Screen 5 | | `mod2 + g` | Throw focused window to screen 5 | | `mod1 + t` | Toggle float for focused window | | `mod1 + i` | Display current layout | | `mod2 + t` | Toggle global tiling | | `mod1 + a` | Select tall layout | | `none` | Select tall-right layout | | `mod1 + s` | Select wide layout | | `none` | Select middle-wide layout | | `mod1 + d` | Select fullscreen layout | | `mod1 + f` | Select column layout | | `none` | Select row layout | | `none` | Select floating layout | | `none` | Select widescreen-tall layout | | `none` | Select bsp layout | ### Available Layouts Amethyst allows you to cycle among several different window layouts. Layouts can also be enabled/disabled to control whether they appear in the cycle sequence at all. #### Tall The default layout. This gives you one "main pane" on the left, and one other pane on the right. By default, one window is placed in the main pane (extending the full height of the screen), and all remaining windows are placed in the other pane. If either pane has more than one window, that pane will be evenly split into rows, to show them all. You can use the keyboard shortcuts above to control which window(s), and how many, are in the main pane, as well as the horizontal size of the main pane vs. the other pane. #### Tall-Right Exactly the same as *Tall*, but the main pane is on the right, with the other pane on the left. #### Wide The rotated version of *Tall*, where the main pane is on the _top_ (extending the full width of the screen), and the other pane is on the bottom. If either pane has more than one window, that pane will split into columns instead of rows. #### Two Pane This layout has two visible panes - the main and the secondary pane. The window in the main pane is pinned, just like in other layouts, and all the remaining windows are placed in the other pane with only one window being visible at a time, which can be swapped (using the keyboard shortcuts). This layout automatically adapts to horizontal/vertical tiling depending on your screen orientation. The main pane is on the left in the horizontal orientation and it's on the top in the vertical orientation. #### Two Pane Right Exactly the same as *Two Pane*, but the main pane is on the right, with the other pane on the left. #### 3Column-Left A three-column version of *Tall*, with one main pane on the left (extending the full height of the screen) and two other panes, one in the middle and one on the right. Like *Tall*, if any pane has more than one window, that pane will be split into rows. You can control how many windows are in the main pane as usual; other windows will be assigned as evenly as possible between the other two panes. #### 3Column-Middle Exactly like *3Column-Left*, but the main pane is in the middle, with the other panes on either side. (In previous versions of Amethyst, this layout was known as *Middle-Wide*.) #### 3Column-Right Exactly like *3Column-Left*, but the main pane is on the right, with the other panes in the middle and on the left. #### Widescreen-Tall This mode is like *Tall*, but if there are multiple windows in the main pane, the main pane splits into columns rather than rows. The other pane still splits windows into rows, like *Tall*. This layout gets its name because it probably makes the most sense on very wide screens, with a large main pane consisting of several columns, and all remaining windows stacked into the final column. Other layouts that work well on very wide screens include any that allow for more than two columns (to take advantage of the screen width), such as any of the *3Column-** layouts, or *Column*. #### Fullscreen In this layout, the currently focused window takes up the entire screen, and the other windows are not visible at all. You can rotate between each of the windows using the "focus the next window" shortcut, as usual. #### Column This layout has one column per window, with each window extending the full height of the screen. The farthest-left window is considered the "main" window in the sense that you can change its size with the "shrink/expand the main pane" shortcuts; the other windows split the remaining space evenly. #### Row The rotated version of *Column*, where each window takes up an entire row, extending the full width of the screen. #### Floating This mode makes all windows "floating", allowing you to move and resize them as if Amethyst were temporarily deactivated. Unlike the other modes, this will mean that windows can be placed "on top of" each other, obscuring your view of some windows. #### Binary Space Partitioning (BSP) This layout does not have a main pane in the way that other layouts do. When adding windows, any given pane can be split evenly into two panes along whatever axis is longer. This is recursive such that pane A can be split in the middle into pane A on the left and pane B on the right; pane B can then be split into pane B on top and pane C on bottom; pane C can then be split into pane C on the left and pane D on the right; and so on. #### Custom (beta) Custom layouts can be implemented via JavaScript. See [Custom Layouts](docs/custom-layouts.md). ### Configuration File Amethyst supports configuration via YAML in the home directory. See [Configuration Files](docs/configuration-files.md). Note that if configuration file is present, it will override the settings defined via the GUI. ## Building Amethyst Locally If you would like to test your changes locally, Amethyst can be built using [`fastlane`](https://github.com/fastlane/fastlane). Just run the command `fastlane` in the root folder, and the app will be available at `./build/Amethyst.app`. (You may need to provision the app under "Signing & Capabilities" in XCode first.) ## Contributing If you'd like to contribute please branch off of the `development` branch and open pull requests against it rather than `master`. Otherwise just try to stick to the general style of the code. ## Donating Amethyst is free and always will be. That said, a couple of people have expressed their desire to donate money in appreciation. Given the current political climate I would recommend donating to one of these organizations instead: * [American Civil Liberties Union](https://www.aclu.org/) * [Planned Parenthood](https://www.plannedparenthood.org/) * [Southern Poverty Law Center](https://www.splcenter.org/) * [National Resources Defense Council](https://www.nrdc.org/) * [International Refugee Assistance Project](https://refugeerights.org/) * [NAACP Legal Defense Fund](https://www.naacpldf.org/) * [The Trevor Project](https://www.thetrevorproject.org/) * [Mexican American Legal Defense Fund](https://www.maldef.org/) * [ProPublica](https://www.propublica.org/) And a bunch of technology-oriented ones: * [National Center for Women & Information Technology](https://ncwit.org/about-ncwit/donate/) * [girls who code](https://girlswhocode.com/get-involved/) * [Trans*H4CK](https://www.transhack.org/sponsorship/) * [Black Girls CODE](https://wearebgc.org/donate/) ================================================ FILE: docs/configuration-files.md ================================================ # Configuration Files Amethyst will pick up a config file located at `~/.amethyst.yml` or `~/.config/amethyst/amethyst.yml` in this order. A sample can be found at [/.amethyst.sample.yml](../.amethyst.sample.yml) ## Configuration Keys | Key | Description | | -- | -- | | `layouts` | Ordered list of layouts to use by layout key (default tall, wide, fullscreen, and column). | | `mod1` | First mod (default option + shift). | | `mod2` | Second mod (default option + shift + control). | | `mod3` | Third mod (not used by default). | | `mod4` | Fourth mod (not used by default). | | `window-max-count` | The max number of windows that may be visible on a screen at one time before additional windows are minimized. A value of 0 disables the feature. | | `window-margins` | Boolean flag for whether or not to add margins between and around windows (default `false`). | | `smart-window-margins` | Boolean flag for whether or not to set window margins if there is only one window on the screen, assuming window margins are enabled (default `false`). | | `window-margin-size` | The size of the margins between and around windows (in px, default `0`). | | `window-minimum-height` | The smallest height that a window can be sized to regardless of its layout frame (in px, default `0`). | | `window-minimum-width` | The smallest width that a window can be sized to regardless of its layout frame (in px, default `0`) | | `floating` | List of bundle identifiers for applications to either be automatically floating or automatically tiled based on `floating-is-blacklist` (default `[]`). | | `floating-is-blacklist` | Boolean flag determining behavior of the `floating` list. `true` if the applications should be floating and all others tiled. `false` if the applications should be tiled and all others floating (default `true`). | | `ignore-menu-bar` | `true` if screen frames should exclude the status bar. `false` if the screen frames should include the status bar (default `false`). | | `float-small-windows` | `true` if windows smaller than the `small-window-size` threshold should be floating by default (default `true`). | | `small-window-size` | Pixel threshold for `float-small-windows`. Windows with both width and height below this value are considered small (in px, default `500`). | | `mouse-follows-focus` | `true` if the mouse should move position to the center of a window when it becomes focused (default `false`). Note that this is largely incompatible with `focus-follows-mouse`. | | `focus-follows-mouse` | `true` if the windows underneath the mouse should become focused as the mouse moves (default `false`). Note that this is largely incompatible with `mouse-follows-focus` | | `mouse-swaps-windows` | `true` if dragging and dropping windows on to each other should swap their positions (default `false`). | | `mouse-resizes-windows` | `true` if changing the frame of a window with the mouse should update the layout to accommodate the change (default `false`). Note that not all layouts will be able to respond to the change. | | `enables-layout-hud` | `true` to display the name of the layout when a new layout is selected (default `true`). | | `enables-layout-hud-on-space-change` | `true` to display the name of the layout when moving to a new space (default `true`). | | `enables-window-count-hud` | `true` to display notifications when window max count changes (default `false`). | | `use-canary-build` | `true` to get updates to beta versions of the software (default `false`). | | `new-windows-to-main` | `true` to insert new windows into the first position and `false` to insert new windows into the last position (default `false`). | | `follow-space-thrown-windows` | `true` to automatically move to a space when throwing a window to it (default `true`). | | `window-resize-step` | The integer percentage of the screen dimension to increment and decrement main pane ratios by (default `5`). | | `screen-padding-left` | Padding to apply between windows and the left edge of the screen (in px, default `0`). | | `screen-padding-right` | Padding to apply between windows and the right edge of the screen (in px, default `0`). | | `screen-padding-top` | Padding to apply between windows and the top edge of the screen (in px, default `0`). | | `screen-padding-bottom` | Padding to apply between windows and the bottom edge of the screen (in px, default `0`). | `restore-layouts-on-launch` | `true` to maintain layout state across application executions (default `true`). | | `debug-layout-info` | `true` to display some optional debug information in the layout HUD (default `false`). | | `disable-padding-on-builtin-display` | `true` to disable screen padding on in-built display (default `false`). | | `hide-menu-bar-icon` | `true` to hide the menu bar icon (default `false`). | ## Commands Commands are defined at the root of the config file, as either an object with `mod` and `key` values to customize the command or is `false` to entirely disable it. | Key | Description | | --- | ----------- | | `mod` | The modifier to use, either `mod1`, `mod2`, `mod3` or `mod4`. | | `key` | The key on the keyboard to use. | ### Mods A mod is a list of keyboard modifiers. Namely, `option`, `control`, `shift`, and `command`. ### Command Keys | Command | Description | | ------- | ------------| | `cycle-layout` | Move to the next layout in the list. | | `cycle-layout-backward` | Move to the previous layout in the list. | | `shrink-main` | Shrink the main pane by a percentage of the screen dimension as defined by `window-resize-step`. Note that not all layouts respond to this command. | | `expand-main` | Expand the main pane by a percentage of the screen dimension as defined by `window-resize-step`. Note that not all layouts respond to this command. | | `increase-main` | Increase the number of windows in the main pane. Note that not all layouts respond to this command. | | `decrease-main` | Decrease the number of windows in the main pane. Note that not all layouts respond to this command. | | `increase-window-max-count` | Increase the maximum number of windows allowed on screen before additional windows are minimized. | | `decrease-window-max-count` | Decrease the maximum number of windows allowed on screen before additional windows are minimized. | | `command1` | General purpose command for custom layouts. Functionality is layout-dependent. | | `command2` | General purpose command for custom layouts. Functionality is layout-dependent. | | `command3` | General purpose command for custom layouts. Functionality is layout-dependent. | | `command4` | General purpose command for custom layouts. Functionality is layout-dependent. | | `focus-ccw` | Focus the next window in the list going counter-clockwise. | | `focus-cw` | Focus the next window in the list going clockwise. | | `focus-main` | Focus the main window in the list. | | `focus-screen-ccw` | Focus the next screen in the list going counter-clockwise. | | `focus-screen-cw` | Focus the next screen in the list going clockwise. | | `swap-screen-ccw` | Move the currently focused window onto the next screen in the list going counter-clockwise. | | `swap-screen-cw` | Move the currently focused window onto the next screen in the list going clockwise. | | `swap-ccw` | Swap the position of the currently focused window with the next window in the list going counter-clockwise. | | `swap-cw` | Swap the position of the currently focused window with the next window in the list going clockwise. | | `swap-main` | Swap the position of the currently focused window with the main window in the list. | | `focus-screen-n` | Move focus to the n-th screen in the list; e.g., `focus-screen-3` will move mouse focus to the 3rd screen. Note that the main window in the given screen will be focused. | | `throw-screen-n` | Move the currently focused window to the n-th screen; e.g., `throw-screen-3` will move the window to the 3rd screen. | | `throw-space-n` | Move the currently focused window to the n-th space; e.g., `throw-space-3` will move the window to the 3rd space. | | `throw-space-left` | Move the currently focused window to the space to the left. | | `throw-space-right` | Move currently the focused window to the space to the right. | | `toggle-float` | Toggle the floating state of the currently focused window; i.e., if it was floating make it tiled and if it was tiled make it floating. | | `display-current-layout` | Display the layout HUD with the current layout on each screen. | | `toggle-tiling` | Turn on or off tiling entirely. | | `enable-tiling` | Turn on tiling. | | `disable-tiling` | Turn off tiling. | | `reevaluate-windows` | Rerun the current layout's algorithm. | | `toggle-focus-follows-mouse` | Turn on or off `focus-follows-mouse`. | | `relaunch-amethyst` | Automatically quit and reopen Amethyst. | ### Layout Selection Amethyst supports defining shortcuts for selecting specific layouts directly. They take the form of `select-${layout_key}-layout`. For example, defining the command `select-tall-layout` will define a shortcut that when used will switch directly to the Tall layout. Note, this works for custom layouts as well. ================================================ FILE: docs/custom-layouts.md ================================================ # Custom Layouts (beta) Amethyst supports implementing custom layouts via JavaScript. ## Installing Layouts are located in `~/Library/Application Support/Amethyst/Layouts/`. This directory is automatically created when Amethyst is first launched. JavaScript files in this directory will automatically be picked up and keyed by the name of the file; e.g., `my-cool-layout.js` will be available as a layout with the key `my-cool-layout`. At the moment, files must be manually moved to this directory. There is no way to import them through the app. ## Defining a Layout At the root of the file you must define a function named `layout`. This function should return an object with the following properties. ### Layout Properties #### `name` A string defining the name of the layout. If no name is specified it will default to the layout key. #### `initialState` An object defining any initial state to be tracked by the layout. #### `commands` An object defining the commands the layout responds to. There are four available custom commands keyed as `command1`, `command2`, `command3`, and `command4`, in addition to paned layout commands keyed as `expandMain`, `shrinkMain`, `increaseMain`, and `decreaseMain`. Commands are objects with a `description` string to describe what the command does and an `updateState` function. The `updateState` function takes two arguments—`state` and `focusedWindowID`—and must return a new state object. * `state`: the current layout state * `focusedWindowID`: the currently focused window #### `extends` The key for a layout that the custom layout extends. Each call to `getFrameAssignments` will include the frames determined by the extended layout for reference. There are some limitations to this extension—you cannot affect the state of the extended layout, for example—but this is useful for small modifications to existing native layout algorithms. #### `getFrameAssignments` A function that takes four arguments—`windows`, `screenFrame`, `state`, and `extendedFrames`—and returns a mapping of window ids to window frames. * `windows`: the list of active windows on the screen * `screenFrame`: the frame of the screen containing the layout * `state`: the current layout state * `extendedFrames`: the frames that are inherited from the extended layout if any is defined The return should be an object with _new_ frames keyed by the window id. #### `updateWithChange` A function that takes two arguments—`change` and `state`—and must return a new layout state based on the provided change. * `change`: the particular change the layout needs to respond to. #### `recommendMainPaneRatio` A function that takes two arguments—`ratio` and `state`—and must return a new layout state based on the recommended ratio. * `ratio`: the ratio recommended for the layout based on windows being resized by mouse controls. ### Mouse Resizing Amethyst supports changing the relative ratios of windows when changing the size of windows by dragging them with the cursor. By default, these ratios are recommended by calling the `recommendMainPaneRatio` layout property, and happen on the horizontal axis. When the window is resized, the system determines what ratio is appropriate for the new width given the dimensions of the screen it is on. These values are clamped to [0, 1]. To scale along a different axis, you can specify the `unconstrainedDimension` and `isMain` properties of each window's frame. The dimension determines the axis along which window frame changes will cause recommended ratio changes, and the `isMain` property determines which part of the ratio the window applies to. Note that currently the recommended ratio is global to the layout and not specific to a given window, so it is not particularly meaningful to specify multiple `unconstrainedDimension` values among frames. ### Common Structures #### Windows A window is an object with three properties. * `id`: an opaque identifier for referencing the window both within `getFrameAssignments` and across the layout state * `frame`: the current frame of the window in the screen space * `isFocused`: boolean for whether or not the window is currently focused #### Frames A frame is an object with four required properties and two optional properties. * `x`: x-coordinate in the screen space * `y`: y-coordinate in the screen space * `width`: pixel width * `height`: pixel height * (optional) `isMain`: boolean indicating whether the window is in the main pane (default: `true`) * (optional) `unconstrainedDimension`: a string indicating on which axis the window is able to be resized via mouse (values: `horizontal`, `vertical`; default: `horizontal`) Note that frames are in a global space, not relative to a given screen. #### Changes A change represents the outcome of an event in the system. It is an object with up to two properties. * `change`: the string key of the type of event (see below) * `windowID`: the window id for relevant changes * `otherWindowID`: the second window id for relevant changes Current changes are: * `"add"`: a window has been added to tracking * Has a `windowID` for the new window * `"remove"`: a window has been removed from tracking * Has a `windowID` for the removed window * `"focus_changed"`: the currently focused window has changed * Has a `windowID` for the newly focused window * `"window_swap"`: two windows have been swapped in position * Has a `windowID` for the first window and an `otherWindowID` for the second window * `"application_activate"`: an application has been activated * No parameters * `"application_deactivate"`: an application has been deactivated * No parameters * `"space_change"`: the current space on a screen has changed * No parameters * `"layout_change"`: the layout of a screen changed * No parameters * `"unknown"`: an unknown event * No parameters ## Examples There are several layouts defined for automated tests that can serve as examples. They are in [AmethystTests/Model/CustomLayouts/](../AmethystTests/Model/CustomLayouts/). ================================================ FILE: docs/troubleshooting.md ================================================ # Troubleshooting ## Nothing is working! Here are some common problems and their solutions. ### "Always float" Amethyst has the option to float everything by default until the user manually intervenes. When this mode is unintentionally enabled it can appear that nothing is working. The option is located in settings in the Floating tab and configured with the `floating-is-blacklist` key in a configuration file. If you have not intentionally enabled this make sure that the option in the Floating tab says "Automatically float applications listed" as in this screenshot. ## One application isn't working! ### "Assign To All Desktops" macOS has the option to assign an application specifically to no Desktops, one Desktop, or all Desktops. Amethyst does not handle the last option very well. To change this setting you can right click (or control click or whatever gesture you may have associated with right click) on the application icon in the Dock. Under Options there is an Assign To section. See the screenshot below for reference. ================================================ FILE: docs/window-limit.md ================================================ # Window Limit ## How To Enable In the General tab of Amethyst's preferences, there is a Maximum Window Count field. Setting this field to a value greater than 0 will enable the feature. ## Behavior * Windows set to float using the keyboard shortcut will not be minimized. * Main windows will not be minimized. Enable ‘Send new windows to main pane’ to have the oldest window cycle out. Disable the setting to exclude main panes, which will act as a persistent workspace alongside other windows. * The window limit applies on a per-screen basis. * Disabling Amethyst will disable window limits. Use the Floating layout to apply window limits without any window tiling. ## Recommendations It is recommended to enable macOS's setting under System Preferences's Dock pane to minimize windows into their dock icon. This will avoid Dock clutter building up while using the feature. To create an iPad-like experience: * Set window limit to 2, and have just fullscreen & 1 "paned layout" enabled. Cycle between layouts to toggle between fullscreen & split view. * Enable "Swap windows using mouse" & "Resize windows using mouse". * Float windows to put them in slide-over mode. ## Limitations * The window limit may not be enforced when windows are moved between screens. Creating a new window will correct the issue. ================================================ FILE: exportOptions.plist ================================================ method developer-id teamID 82P2XLB4UH compileBitcode destination export ================================================ FILE: fastlane/Appfile ================================================ # app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app # apple_id("[[APPLE_ID]]") # Your Apple email address # For more information about the Appfile, see: # https://docs.fastlane.tools/advanced/#appfile ================================================ FILE: fastlane/Fastfile ================================================ # This file contains the fastlane.tools configuration # You can find the documentation at https://docs.fastlane.tools # # For a list of all available actions, check out # # https://docs.fastlane.tools/actions # # For a list of all available plugins, check out # # https://docs.fastlane.tools/plugins/available-plugins # # Uncomment the line if you want fastlane to automatically update itself # update_fastlane default_platform(:mac) lane :mac do build_mac_app xcodebuild( export_archive: true, archive_path: "./build/Amethyst.xcarchive", export_path: "./build", export_options_plist: "./exportOptions.plist" ) end ================================================ FILE: fastlane/Gymfile ================================================ archive_path("./build/Amethyst") scheme("Amethyst") output_directory("./build") ================================================ FILE: fastlane/README.md ================================================ fastlane documentation ---- # Installation Make sure you have the latest version of the Xcode command line tools installed: ```sh xcode-select --install ``` For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions ### mac ```sh [bundle exec] fastlane mac ``` ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). ================================================ FILE: privacy-policy.md ================================================ Amethyst Privacy Policy ----------------------- Amethyst does not collect any personal information and does not transmit any information over a network.