Showing preview only (951K chars total). Download the full file or copy to clipboard to get everything.
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: <NONE>
# key: <NONE>
# General purpose command for custom layouts. Functionality is layout-dependent.
# command2:
# mod: <NONE>
# key: <NONE>
# General purpose command for custom layouts. Functionality is layout-dependent.
# command3:
# mod: <NONE>
# key: <NONE>
# General purpose command for custom layouts. Functionality is layout-dependent.
# command4:
# mod: <NONE>
# key: <NONE>
# 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-<screen-number>:
# 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-<screen-number>:
# 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-<screen-number>:
# 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: <NONE>
# Turn off tiling.
# disable-tiling:
# mod: mod2
# key: <NONE>
# 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
<key>LSUIElement</key>
<true/>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>SUCanaryFeedURL</key>
<string>https://ianyh.com/amethyst/canary-appcast.xml</string>
<key>SUFeedURL</key>
<string>https://ianyh.com/amethyst/appcast.xml</string>
</dict>
</plist>
================================================
FILE: Amethyst/Amethyst.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
================================================
FILE: Amethyst/AmethystDebug.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
================================================
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<SIApplication>?
private var hotKeyManager: HotKeyManager<SIApplication>?
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<SIApplication>.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<WindowManager<SIApplication>>? = {
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="494" id="495"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<menu id="536">
<items>
<menuItem title="Version" enabled="NO" id="bNZ-Ry-Q4Q">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="uP9-ch-b7n"/>
<menuItem title="Disable Tiling" id="rjJ-w4-5Ht">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGlobalTiling:" target="494" id="3aM-9U-hWx"/>
</connections>
</menuItem>
<menuItem title="Layouts" id="layouts-menu-item">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" id="layouts-submenu">
<items/>
</menu>
</menuItem>
<menuItem title="Start Amethyst on Login" id="549">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleStartAtLogin:" target="494" id="550"/>
</connections>
</menuItem>
<menuItem title="Preferences..." id="utO-om-0eu">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="showPreferencesWindow:" target="494" id="DhJ-Jj-71M"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="548"/>
<menuItem title="Check for Updates..." id="554">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="checkForUpdates:" target="494" id="l9Y-Ct-Bzq"/>
</connections>
</menuItem>
<menuItem title="Reset Layouts" id="8DH-wG-R1x">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="resetLayouts:" target="494" id="Osy-WG-Dy8"/>
</connections>
</menuItem>
<menuItem title="Relaunch Amethyst" id="crd-PN-Vy2">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="relaunch:" target="494" id="EeN-oj-Mtv"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="553"/>
<menuItem title="Quit Amethyst" keyEquivalent="q" id="545">
<connections>
<action selector="terminate:" target="-3" id="546"/>
</connections>
</menuItem>
</items>
<point key="canvasLocation" x="-179" y="92"/>
</menu>
<customObject id="494" customClass="AppDelegate" customModule="Amethyst" customModuleProvider="target">
<connections>
<outlet property="preferencesWindowController" destination="0fi-bi-huz" id="dfn-JC-QBQ"/>
<outlet property="startAtLoginMenuItem" destination="549" id="551"/>
<outlet property="statusItemMenu" destination="536" id="547"/>
<outlet property="toggleGlobalTilingMenuItem" destination="rjJ-w4-5Ht" id="4Dq-e8-R1M"/>
<outlet property="versionMenuItem" destination="bNZ-Ry-Q4Q" id="jXC-pY-N2a"/>
<outlet property="layoutsMenuItem" destination="layouts-menu-item" id="layouts-menu-outlet"/>
</connections>
</customObject>
<customObject id="420" customClass="NSFontManager"/>
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" toolbarStyle="expanded" id="pO4-7U-3HA" customClass="PreferencesWindow" customModule="Amethyst" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<rect key="contentRect" x="446" y="278" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="875"/>
<view key="contentView" id="IHj-fa-gIc">
<rect key="frame" x="0.0" y="0.0" width="480" height="260"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<toolbar key="toolbar" implicitIdentifier="2383369F-98A6-4420-8EFE-319DB9588C53" autosavesConfiguration="NO" allowsUserCustomization="NO" showsBaselineSeparator="NO" displayMode="iconAndLabel" sizeMode="regular" id="5oI-m4-akz">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="D4FDE95C-2F3E-4C93-8777-9FBAD1EDB613" explicitItemIdentifier="general" label="General" paletteLabel="General" selectable="YES" id="WV2-G9-AL4">
<imageReference key="image" image="text.and.command.macwindow" catalog="system" symbolScale="large"/>
<size key="minSize" width="22" height="22"/>
<size key="maxSize" width="22" height="22"/>
<connections>
<action selector="selectPane:" target="0fi-bi-huz" id="bq3-bL-T4h"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="C7D5B660-7047-4353-8F39-18609BCF05D1" explicitItemIdentifier="layouts" label="Layouts" paletteLabel="Layouts" selectable="YES" id="FsX-hN-WZb" userLabel="Layouts">
<imageReference key="image" image="uiwindow.split.2x1" catalog="system" symbolScale="large"/>
<size key="minSize" width="22" height="22"/>
<size key="maxSize" width="22" height="22"/>
<connections>
<action selector="selectPane:" target="0fi-bi-huz" id="mBI-Du-M23"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="04C17AAF-5F6B-422F-8592-E8AA78DEB9C7" explicitItemIdentifier="shortcuts" label="Shortcuts" paletteLabel="Shortcuts" tag="1" selectable="YES" id="j0a-87-Yvz">
<imageReference key="image" image="123.rectangle" catalog="system" symbolScale="large"/>
<size key="minSize" width="22" height="22"/>
<size key="maxSize" width="22" height="22"/>
<connections>
<action selector="selectPane:" target="0fi-bi-huz" id="9kO-ms-dfq"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="74318EC6-A099-4964-B13B-6B2B699C7EC3" explicitItemIdentifier="mouse" label="Mouse" paletteLabel="Mouse" selectable="YES" id="dfc-D6-FsG" userLabel="Mouse">
<imageReference key="image" image="dots.and.line.vertical.and.cursorarrow.rectangle" catalog="system" symbolScale="large"/>
<size key="minSize" width="22" height="22"/>
<size key="maxSize" width="22" height="22"/>
<connections>
<action selector="selectPane:" target="0fi-bi-huz" id="0vo-hl-zVN"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="A7FE5E71-A3DC-43B8-92B2-5CF107F2DB2F" explicitItemIdentifier="floating" label="Floating" paletteLabel="Floating" tag="-1" selectable="YES" id="QEp-pN-Lz9">
<imageReference key="image" image="macwindow.on.rectangle" catalog="system" symbolScale="large"/>
<size key="minSize" width="22" height="22"/>
<size key="maxSize" width="22" height="22"/>
<connections>
<action selector="selectPane:" target="0fi-bi-huz" id="WEg-vF-5FE"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="319E6DB9-3D79-40BF-95A2-B84B5236FF8B" explicitItemIdentifier="debug" label="Debug" paletteLabel="Debug" tag="-1" selectable="YES" id="KUo-2v-ywG">
<imageReference key="image" image="waveform.path.ecg.rectangle" catalog="system" symbolScale="large"/>
<size key="minSize" width="22" height="22"/>
<size key="maxSize" width="22" height="22"/>
<connections>
<action selector="selectPane:" target="0fi-bi-huz" id="aNs-0B-bfi"/>
</connections>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="WV2-G9-AL4"/>
<toolbarItem reference="FsX-hN-WZb"/>
<toolbarItem reference="j0a-87-Yvz"/>
<toolbarItem reference="dfc-D6-FsG"/>
<toolbarItem reference="QEp-pN-Lz9"/>
<toolbarItem reference="KUo-2v-ywG"/>
</defaultToolbarItems>
<connections>
<outlet property="delegate" destination="0fi-bi-huz" id="zer-Uh-aco"/>
</connections>
</toolbar>
<connections>
<outlet property="closeMenuItem" destination="qQS-B4-nPy" id="4g6-N2-DxV"/>
<outlet property="delegate" destination="494" id="OGF-zj-QHD"/>
</connections>
<point key="canvasLocation" x="606" y="152"/>
</window>
<customObject id="0fi-bi-huz" userLabel="Preferences Window Controller" customClass="PreferencesWindowController" customModule="Amethyst" customModuleProvider="target">
<connections>
<outlet property="window" destination="pO4-7U-3HA" id="9gm-N1-3ZQ"/>
</connections>
</customObject>
<menu id="zK2-oi-EfJ">
<items>
<menuItem title="Close" keyEquivalent="w" id="qQS-B4-nPy"/>
</items>
<point key="canvasLocation" x="560" y="-126"/>
</menu>
</objects>
<resources>
<image name="123.rectangle" catalog="system" width="24" height="18"/>
<image name="dots.and.line.vertical.and.cursorarrow.rectangle" catalog="system" width="24" height="25"/>
<image name="macwindow.on.rectangle" catalog="system" width="25" height="20"/>
<image name="text.and.command.macwindow" catalog="system" width="24" height="18"/>
<image name="uiwindow.split.2x1" catalog="system" width="24" height="18"/>
<image name="waveform.path.ecg.rectangle" catalog="system" width="24" height="18"/>
</resources>
</document>
================================================
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() ?? "<no title>")
Bundle Identifier: \(application.bundleIdentifier ?? "<no id>")
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 ?? "<unknown name>") (\($0.bundleIdentifier ?? "<unknown bundle id>"))" }
.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 ? "<redacted>" : title() ?? "<no 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() ?? "<no 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<Application: ApplicationType>: 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<Application>, configuration: UserConfiguration, appDelegate: AppDelegate) {
constructCommandWithCommandKey(CommandKey.cycleLayoutForward.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.cycleLayoutForward()
}
constructCommandWithCommandKey(CommandKey.cycleLayoutBackward.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.cycleLayoutBackward()
}
constructCommandWithCommandKey(CommandKey.shrinkMain.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.updateCurrentLayout { layout in
if let panedLayout = layout as? PanedLayout {
panedLayout.shrinkMainPane()
}
}
}
constructCommandWithCommandKey(CommandKey.expandMain.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.updateCurrentLayout { layout in
if let panedLayout = layout as? PanedLayout {
panedLayout.expandMainPane()
}
}
}
constructCommandWithCommandKey(CommandKey.increaseMain.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.updateCurrentLayout { layout in
if let panedLayout = layout as? PanedLayout {
panedLayout.increaseMainPaneCount()
}
}
}
constructCommandWithCommandKey(CommandKey.decreaseMain.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.updateCurrentLayout { layout in
if let panedLayout = layout as? PanedLayout {
panedLayout.decreaseMainPaneCount()
}
}
}
constructCommandWithCommandKey(CommandKey.command1.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.updateCurrentLayout { layout in
if let customLayout = layout as? CustomLayout {
customLayout.command1()
}
}
}
constructCommandWithCommandKey(CommandKey.command2.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.updateCurrentLayout { layout in
if let customLayout = layout as? CustomLayout {
customLayout.command2()
}
}
}
constructCommandWithCommandKey(CommandKey.command3.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = windowManager.focusedScreenManager()
screenManager?.updateCurrentLayout { layout in
if let customLayout = layout as? CustomLayout {
customLayout.command3()
}
}
}
constructCommandWithCommandKey(CommandKey.command4.rawValue) {
let screenManager: ScreenManager<WindowManager<Application>>? = 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<Application.Window>.availableLayoutStrings().forEach { (layoutKey, _) in
self.constructCommandWithCommandKey(UserConfiguration.constructLayoutKeyString(layoutKey)) {
let screenManager: ScreenManager<WindowManager<Application>>? = 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<UCKeyboardLayout> = unsafeBitCast(CFDataGetBytePtr(layoutData), to: UnsafePointer<UCKeyboardLayout>.self)
var keysDown: UInt32 = 0
var chars: [UniChar] = [0, 0, 0, 0]
var realLength: Int = 0
for keyCode in (0..<AMKeyCodeInvalid) {
switch keyCode {
case
kVK_ANSI_Keypad0,
kVK_ANSI_Keypad1,
kVK_ANSI_Keypad2,
kVK_ANSI_Keypad3,
kVK_ANSI_Keypad4,
kVK_ANSI_Keypad5,
kVK_ANSI_Keypad6,
kVK_ANSI_Keypad7,
kVK_ANSI_Keypad8,
kVK_ANSI_Keypad9:
continue
default:
break
}
UCKeyTranslate(layout,
UInt16(keyCode),
UInt16(kUCKeyActionDisplay),
0,
UInt32(LMGetKbdType()),
UInt32(kUCKeyTranslateNoDeadKeysBit),
&keysDown,
chars.count,
&realLength,
&chars)
let string = CFStringCreateWithCharacters(kCFAllocatorDefault, chars, realLength) as String
if let keyCodes = stringToKeyCodes[string] {
var mutableKeyCodes = keyCodes
mutableKeyCodes.append(keyCode)
stringToKeyCodes[string] = mutableKeyCodes
} else {
stringToKeyCodes[string] = [keyCode]
}
}
// Add codes for non-printable characters. They are not printable so they
// are not generated from the keyboard layout data.
stringToKeyCodes["space"] = [kVK_Space]
stringToKeyCodes["enter"] = [kVK_Return]
stringToKeyCodes["up"] = [kVK_UpArrow]
stringToKeyCodes["right"] = [kVK_RightArrow]
stringToKeyCodes["down"] = [kVK_DownArrow]
stringToKeyCodes["left"] = [kVK_LeftArrow]
stringToKeyCodes["delete"] = [kVK_Delete]
return stringToKeyCodes
}
private func constructCommandWithCommandKey(_ commandKey: String, handler: @escaping HotKeyHandler) {
userConfiguration.constructCommand(for: self, commandKey: commandKey, handler: handler)
}
private func carbonModifiersFromModifiers(_ modifiers: UInt) -> 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<Application.Window>.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<Window: WindowType>: 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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>, on screen: Screen) -> LayoutWindow<Window>? {
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<Window>, on screen: Screen) -> FrameAssignment<Window>? {
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<Window>)`
- `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<Window: WindowType>: Layout<Window> {
/**
Updates internal state of the layout based on a window change.
- Parameters:
- windowChange: A `WindowChange`.
*/
func updateWithChange(_ windowChange: Change<Window>) {
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<Window: WindowType>: 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<Window: WindowType>: StatefulLayout<Window> {
typealias WindowID = Window.WindowID
private typealias TraversalNode = (node: TreeNode<Window>, 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<Window>()
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<Window>.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<Window>]) {
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<Window>) {
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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
let windows = windowSet.windows
guard !windows.isEmpty else {
return []
}
if rootNode.left == nil && rootNode.right == nil {
constructInitialTreeWithWindows(windows)
}
let windowIDMap: [WindowID: LayoutWindow<Window>] = windows.reduce([:]) { (windowMap, window) -> [WindowID: LayoutWindow<Window>] in
var mutableWindowMap = windowMap
mutableWindowMap[window.id] = window
return mutableWindowMap
}
let baseFrame = screen.adjustedFrame()
var ret: [FrameAssignmentOperation<Window>] = []
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<Window>(
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<Window>, rhs: BinarySpacePartitioningLayout<Window>) -> 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<Window: WindowType>: Layout<Window>, 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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType> {
let id: String
let window: LayoutWindow<Window>
}
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<Window: WindowType> {
case none
case layout(Layout<Window>)
}
class CustomLayout<Window: WindowType>: StatefulLayout<Window>, 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<Window> = {
guard let extendedLayoutKey = self.layout?.objectForKeyedSubscript("extends"), extendedLayoutKey.isString else {
return .none
}
guard let layout = LayoutType<Window>.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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
switch layoutExtension {
case .none:
return nil
case .layout(let layout):
return layout.frameAssignments(windowSet, on: screen)
}
}
override func frameAssignments(_ windowSet: WindowSet<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
let windows = windowSet.windows
guard !windows.isEmpty else {
return []
}
let screenFrame = screen.adjustedFrame()
let jsScreenFrameArg = JSValue(rect: screenFrame, in: context)!
let jsWindows: [WindowID: JSWindow<Window>] = windows.reduce([:]) { partialResult, layoutWindow in
let id = idHash(forWindowID: layoutWindow.id) ?? UUID().uuidString
let window = JSWindow<Window>(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<Window>? 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<Window>(
frame: frame.toRoundedRect(),
window: jsWindow.window,
screenFrame: screenFrame,
resizeRules: resizeRules
)
return FrameAssignmentOperation(frameAssignment: frameAssignment, windowSet: windowSet)
}
}
override func updateWithChange(_ windowChange: Change<Window>) {
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<Window>) -> [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<Window: WindowType>: Layout<Window> {
override static var layoutName: String { return "Floating" }
override static var layoutKey: String { return "floating" }
override var layoutDescription: String { return "" }
override func frameAssignments(_ windowSet: WindowSet<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window: WindowType>: Layout<Window> {
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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: FourColumnLayout<Window>, 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<Window: WindowType>: FourColumnLayout<Window>, 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<Window: WindowType>: Layout<Window> {
override static var layoutName: String { return "Fullscreen" }
override static var layoutKey: String { return "fullscreen" }
override var layoutDescription: String { return "" }
override func frameAssignments(_ windowSet: WindowSet<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>(
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<Window: WindowType>: Layout<Window>, 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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: Layout<Window>, 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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: Layout<Window>, 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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: Layout<Window> {
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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: ThreeColumnLayout<Window>, 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<Window: WindowType>: ThreeColumnLayout<Window>, 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<Window: WindowType>: ThreeColumnLayout<Window>, 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<Window: WindowType>: Layout<Window>, 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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: Layout<Window>, 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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: Layout<Window>, 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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: Layout<Window> {
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<Window>, on screen: Screen) -> [FrameAssignmentOperation<Window>]? {
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<Window>] 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<Window>(
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<Window: WindowType>: WidescreenTallLayout<Window> {
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<Window: WindowType>: WidescreenTallLayout<Window> {
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<Window: WindowType>: 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<Window: WindowType> {
let windows: [LayoutWindow<Window>]
private let isWindowWithIDActive: (Window.WindowID) -> Bool
private let isWindowWithIDFloating: (Window.WindowID) -> Bool
private let windowForID: (Window.WindowID) -> Window?
init(
windows: [LayoutWindow<Window>],
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<Window>) -> Bool {
return isWindowWithIDActive(window.id)
}
func isWindowFloating(_ window: LayoutWindow<Window>) -> Bool {
return isWindowWithIDFloating(window.id)
}
func perform(frameAssignment: FrameAssignment<Window>) {
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<Window: WindowType>: Operation {
let frameAssignment: FrameAssignment<Window>
let windowSet: WindowSet<Window>
init(frameAssignment: FrameAssignment<Window>, windowSet: WindowSet<Window>) {
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<Window: WindowType> {
/// The frame to apply to the window.
let frame: CGRect
/// The window that will be moved and sized.
let window: LayoutWindow<Window>
/// 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<Window>, screenFrame: CGRect, resizeRules: ResizeRules) {
self.frame = frame
self.window = window
self.screenFrame = screenFrame
self.resizeRules = resizeRules
self.disableWindowMargins = false
}
init(frame: CGRect, window: LayoutWindow<Window>, 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<Delegate: FocusFollowsMouseManagerDelegate> {
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<Window: WindowType> {
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<Window>)
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<Target: FocusTransitionTarget> {
typealias Window = Target.Window
typealias Screen = Window.Screen
weak var target: Target?
private let userConfiguration: UserConfiguration
private let focusFollowsMouseManager: FocusFollowsMouseManager<FocusTransitionCoordinator<Target>>
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<Window: WindowType> {
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<Window>] {
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<Window>.Type {
switch self {
case .tall:
return TallLayout<Window>.self
case .tallRight:
return TallRightLayout<Window>.self
case .wide:
return WideLayout<Window>.self
case .twoPane:
return TwoPaneLayout<Window>.self
case .twoPaneRight:
return TwoPaneRightLayout<Window>.self
case .threeColumnLeft:
return ThreeColumnLeftLayout<Window>.self
case .threeColumnMiddle:
return ThreeColumnMiddleLayout<Window>.self
case .threeColumnRight:
return ThreeColumnRightLayout<Window>.self
case .fourColumnLeft:
return FourColumnLeftLayout<Window>.self
case .fourColumnRight:
return FourColumnRightLayout<Window>.self
case .fullscreen:
return FullscreenLayout<Window>.self
case .column:
return ColumnLayout<Window>.self
case .row:
return RowLayout<Window>.self
case .floating:
return FloatingLayout<Window>.self
case .widescreenTallLeft:
return WidescreenTallLayoutLeft<Window>.self
case .widescreenTallRight:
return WidescreenTallLayoutRight<Window>.self
case .binarySpacePartitioning:
return BinarySpacePartitioningLayout<Window>.self
case .custom:
return CustomLayout<Window>.self
}
}
static func from(key: String) -> LayoutType<Window> {
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<Window>? {
let type = LayoutType<Window>.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<Window>(key: layoutKey, fileURL: layoutFile)
} catch {
return nil
}
}
static func layoutNameForKey(_ layoutKey: String) -> String? {
let type = LayoutType<Window>.from(key: layoutKey)
guard case .custom = type else {
return type.layoutClass.layoutName
}
return layoutForKey(layoutKey)?.layoutName
}
static func standardLayoutClasses() -> [Layout<Window>.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<Window>] {
let layoutKeys: [String] = userConfiguration.layoutKeys()
let layouts = layoutKeys.map { layoutKey -> Layout<Window>? 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<Window>) throws -> Data {
return try JSONEncoder().encode(layout)
}
static func decoded(data: Data, key: String) throws -> Layout<Window> {
let layoutType = LayoutType<Window>.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<Self>, minimizingIn range: (_ windowCount: Int) -> Range<Int>)
func activeWindowSet(forScreenManager screenManager: ScreenManager<Self>) -> WindowSet<Window>
func onReflowInitiation()
func onReflowCompletion()
}
final class ScreenManager<Delegate: ScreenManagerDelegate>: 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<Window>] = []
private var currentLayoutIndexBySpaceUUID: [String: Int] = [:]
private var layoutsBySpaceUUID: [String: [Layout<Window>]] = [:]
private var currentLayoutIndex: Int = 0
var previousLayoutKey: String?
var currentLayout: Layout<Window>? {
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<Window>] in
return try ScreenManager<Delegate>.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<Window>] {
let layouts: [Layout<Window>] = LayoutType.layoutsWithConfiguration(userConfiguration)
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
SYMBOL INDEX (9 symbols across 9 files)
FILE: AmethystTests/Model/CustomLayouts/extended.js
function layout (line 1) | function layout() {
FILE: AmethystTests/Model/CustomLayouts/fullscreen.js
function layout (line 1) | function layout() {
FILE: AmethystTests/Model/CustomLayouts/null.js
function layout (line 1) | function layout() {
FILE: AmethystTests/Model/CustomLayouts/recommended-main-pane-ratio.js
function layout (line 1) | function layout() {
FILE: AmethystTests/Model/CustomLayouts/static-ratio-tall-native-commands.js
function layout (line 1) | function layout() {
FILE: AmethystTests/Model/CustomLayouts/static-ratio-tall.js
function layout (line 1) | function layout() {
FILE: AmethystTests/Model/CustomLayouts/subset.js
function layout (line 1) | function layout() {
FILE: AmethystTests/Model/CustomLayouts/undefined.js
function layout (line 1) | function layout() {
FILE: AmethystTests/Model/CustomLayouts/uniform-columns.js
function layout (line 1) | function layout() {
Condensed preview — 146 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (973K chars).
[
{
"path": ".amethyst.sample.yml",
"chars": 9899,
"preview": "# Default settings for Amethyst\n# Repo: `https://github.com/ianyh/Amethyst`\n#\n# Note due to issue 1419 (https://github.c"
},
{
"path": ".eslintrc.yml",
"chars": 73,
"preview": "---\n env:\n es6: true\n parserOptions:\n ecmaVersion: 9\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 773,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/workflows/tests.yml",
"chars": 429,
"preview": "name: Tests\n\non:\n push:\n branches: [ development ]\n pull_request:\n\njobs:\n build:\n name: Build and run unit test"
},
{
"path": ".gitignore",
"chars": 337,
"preview": "# Xcode\n.DS_Store\n*/build/*\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspecti"
},
{
"path": ".hound.yml",
"chars": 101,
"preview": "swiftlint:\n config_file: .swiftlint.yml\n\neslint:\n enabled: true\n config_file: .eslintrc.yml\n"
},
{
"path": ".swiftlint.yml",
"chars": 406,
"preview": "disabled_rules:\n - function_body_length\n - closing_brace\n - statement_position\n - force_cast\n - force_try"
},
{
"path": "Amethyst/Amethyst-Bridging-Header.h",
"chars": 0,
"preview": ""
},
{
"path": "Amethyst/Amethyst-Info.plist",
"chars": 1260,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Amethyst/Amethyst.entitlements",
"chars": 181,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Amethyst/AmethystDebug.entitlements",
"chars": 311,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Amethyst/AppDelegate.swift",
"chars": 10205,
"preview": "//\n// AppDelegate.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/8/16.\n// Copyright © 2016 Ian Ynda-Hummel."
},
{
"path": "Amethyst/Base.lproj/MainMenu.xib",
"chars": 12039,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
},
{
"path": "Amethyst/Categories/NSRunningApplication+Manageable.swift",
"chars": 1883,
"preview": "//\n// NSRunningApplication+Manageable.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/8/16.\n// Copyright © 2"
},
{
"path": "Amethyst/Categories/NSTableView+Amethyst.swift",
"chars": 233,
"preview": "//\n// NSTableView+Amethyst.swift\n// Amethyst\n//\n// Created by James Zaghini on 15/5/18.\n// Copyright © 2018 Ian Ynda"
},
{
"path": "Amethyst/Debug/AppsInfo.swift",
"chars": 914,
"preview": "//\n// AppsInfo.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/9/23.\n// Copyright © 2023 Ian Ynda-Hummel. Al"
},
{
"path": "Amethyst/Debug/DebugInfo.swift",
"chars": 2159,
"preview": "//\n// DebugInfo.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 2/24/20.\n// Copyright © 2020 Ian Ynda-Hummel. "
},
{
"path": "Amethyst/Debug/ScreensInfo.swift",
"chars": 442,
"preview": "//\n// ScreensInfo.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/9/23.\n// Copyright © 2023 Ian Ynda-Hummel."
},
{
"path": "Amethyst/Debug/WindowsInfo.swift",
"chars": 1580,
"preview": "//\n// WindowsInfo.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/5/23.\n// Copyright © 2023 Ian Ynda-Hummel."
},
{
"path": "Amethyst/Events/HotKeyManager.swift",
"chars": 19798,
"preview": "//\n// HotKeyManager.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright © 2016 Ian Ynda-Humm"
},
{
"path": "Amethyst/Images.xcassets/123.rectangle.imageset/Contents.json",
"chars": 163,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"123.rectangle.svg\",\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \""
},
{
"path": "Amethyst/Images.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1301,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"icon_16x16.png\",\n \"idiom\" : \"mac\",\n \"scale\" : \"1x\",\n \"size\" : "
},
{
"path": "Amethyst/Images.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Amethyst/Images.xcassets/amethyst.imageset/Contents.json",
"chars": 273,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"icon_32x32.png\",\n \"idiom\" : \"mac\",\n \"scale\" : \"1x\"\n },\n {\n "
},
{
"path": "Amethyst/Images.xcassets/dots.and.line.vertical.and.cursorarrow.rectangle.imageset/Contents.json",
"chars": 198,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"dots.and.line.vertical.and.cursorarrow.rectangle.svg\",\n \"idiom\" : \"unive"
},
{
"path": "Amethyst/Images.xcassets/icon-statusitem-disabled.imageset/Contents.json",
"chars": 301,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"icon-statusitem-disabled.png\",\n \"idiom\" : \"mac\",\n \"scale\" : \"1x\"\n "
},
{
"path": "Amethyst/Images.xcassets/icon-statusitem.imageset/Contents.json",
"chars": 283,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"icon-statusitem.png\",\n \"idiom\" : \"mac\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "Amethyst/Images.xcassets/macwindow.on.rectangle.imageset/Contents.json",
"chars": 172,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"macwindow.on.rectangle.svg\",\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" "
},
{
"path": "Amethyst/Images.xcassets/text.and.command.macwindow.imageset/Contents.json",
"chars": 176,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"text.and.command.macwindow.svg\",\n \"idiom\" : \"universal\"\n }\n ],\n \"in"
},
{
"path": "Amethyst/Images.xcassets/uiwindow.split.2x1.imageset/Contents.json",
"chars": 168,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"uiwindow.split.2x1.svg\",\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n"
},
{
"path": "Amethyst/Images.xcassets/waveform.path.ecg.rectangle.imageset/Contents.json",
"chars": 177,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"waveform.path.ecg.rectangle.svg\",\n \"idiom\" : \"universal\"\n }\n ],\n \"i"
},
{
"path": "Amethyst/Layout/Layout.swift",
"chars": 8403,
"preview": "//\n// Layout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/3/15.\n// Copyright © 2015 Ian Ynda-Hummel. All"
},
{
"path": "Amethyst/Layout/Layouts/BinarySpacePartitioningLayout.swift",
"chars": 13281,
"preview": "//\n// BinarySpacePartitioningLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/29/16.\n// Copyright © 20"
},
{
"path": "Amethyst/Layout/Layouts/ColumnLayout.swift",
"chars": 3960,
"preview": "//\n// ColumnLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/14/15.\n// Copyright © 2015 Ian Ynda-Humm"
},
{
"path": "Amethyst/Layout/Layouts/CustomLayout.swift",
"chars": 13887,
"preview": "//\n// CustomLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 7/2/21.\n// Copyright © 2021 Ian Ynda-Hummel"
},
{
"path": "Amethyst/Layout/Layouts/FloatingLayout.swift",
"chars": 567,
"preview": "//\n// FloatingLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/14/15.\n// Copyright © 2015 Ian Ynda-Hu"
},
{
"path": "Amethyst/Layout/Layouts/FourColumnLayout.swift",
"chars": 10637,
"preview": "//\n// FourColumnLayout.swift\n// Amethyst\n//\n// Originally created by Ian Ynda-Hummel on 12/15/15.\n// Copyright © 201"
},
{
"path": "Amethyst/Layout/Layouts/FullscreenLayout.swift",
"chars": 1249,
"preview": "//\n// FullscreenLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/14/15.\n// Copyright © 2015 Ian Ynda-"
},
{
"path": "Amethyst/Layout/Layouts/RowLayout.swift",
"chars": 3974,
"preview": "//\n// RowLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/14/15.\n// Copyright © 2015 Ian Ynda-Hummel."
},
{
"path": "Amethyst/Layout/Layouts/TallLayout.swift",
"chars": 3997,
"preview": "//\n// TallLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/14/15.\n// Copyright © 2015 Ian Ynda-Hummel"
},
{
"path": "Amethyst/Layout/Layouts/TallRightLayout.swift",
"chars": 4044,
"preview": "//\n// TallRightLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/14/15.\n// Copyright © 2015 Ian Ynda-H"
},
{
"path": "Amethyst/Layout/Layouts/ThreeColumnLayout.swift",
"chars": 9837,
"preview": "//\n// ThreeColumnLayout.swift\n// Amethyst\n//\n// Originally created by Ian Ynda-Hummel on 12/15/15.\n// Copyright © 20"
},
{
"path": "Amethyst/Layout/Layouts/TwoPaneLayout.swift",
"chars": 4007,
"preview": "//\n// TwoPaneLayout.swift\n// Amethyst\n//\n// Created by @mwz on 10/06/2021.\n// Copyright © 2021 Ian Ynda-Hummel. All "
},
{
"path": "Amethyst/Layout/Layouts/TwoPaneRightLayout.swift",
"chars": 4032,
"preview": "//\n// TwoPaneRightLayout.swift\n// Amethyst\n//\n// Created by Anja on 16.06.23.\n// Copyright © 2023 Ian Ynda-Hummel. A"
},
{
"path": "Amethyst/Layout/Layouts/WideLayout.swift",
"chars": 3943,
"preview": "//\n// WideLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/14/15.\n// Copyright © 2015 Ian Ynda-Hummel"
},
{
"path": "Amethyst/Layout/Layouts/WidescreenTallLayout.swift",
"chars": 4921,
"preview": "//\n// WidescreenTallLayout.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/15/15.\n// Copyright © 2015 Ian Y"
},
{
"path": "Amethyst/Layout/ReflowOperation.swift",
"chars": 8335,
"preview": "//\n// ReflowOperation.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/19/19.\n// Copyright © 2019 Ian Ynda-Hu"
},
{
"path": "Amethyst/Managers/AppManager.swift",
"chars": 656,
"preview": "//\n// relaunch.swift\n// Amethyst\n//\n// Created by Agustin Suarez on 2021-02-23.\n// Copyright © 2021 Ian Ynda-Hummel."
},
{
"path": "Amethyst/Managers/FocusFollowsMouseManager.swift",
"chars": 2998,
"preview": "//\n// FocusFollowsMouseManager.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright © 2016 Ia"
},
{
"path": "Amethyst/Managers/FocusTransitionCoordinator.swift",
"chars": 6626,
"preview": "//\n// FocusTransitionCoordinator.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/24/19.\n// Copyright © 2019 "
},
{
"path": "Amethyst/Managers/HotKeyRegistrar.swift",
"chars": 2452,
"preview": "//\n// HotKeyRegistrar.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright © 2016 Ian Ynda-Hu"
},
{
"path": "Amethyst/Managers/LayoutType.swift",
"chars": 8804,
"preview": "//\n// LayoutManager.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright © 2016 Ian Ynda-Humm"
},
{
"path": "Amethyst/Managers/LogManager.swift",
"chars": 195,
"preview": "//\n// LogManager.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/19/16.\n// Copyright © 2016 Ian Ynda-Hummel."
},
{
"path": "Amethyst/Managers/ScreenManager.swift",
"chars": 15293,
"preview": "//\n// ScreenManager.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 12/23/15.\n// Copyright © 2015 Ian Ynda-Hum"
},
{
"path": "Amethyst/Managers/Screens.swift",
"chars": 4310,
"preview": "//\n// Screens.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/29/19.\n// Copyright © 2019 Ian Ynda-Hummel. Al"
},
{
"path": "Amethyst/Managers/WindowManager.swift",
"chars": 42605,
"preview": "//\n// WindowManager.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/14/16.\n// Copyright © 2016 Ian Ynda-Humm"
},
{
"path": "Amethyst/Managers/WindowTransitionCoordinator.swift",
"chars": 7078,
"preview": "//\n// WindowTransitionCoordinator.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/24/19.\n// Copyright © 2019"
},
{
"path": "Amethyst/Managers/Windows.swift",
"chars": 8271,
"preview": "//\n// Windows.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 9/15/19.\n// Copyright © 2019 Ian Ynda-Hummel. Al"
},
{
"path": "Amethyst/Model/Application.swift",
"chars": 7421,
"preview": "//\n// Application.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/12/19.\n// Copyright © 2019 Ian Ynda-Hummel"
},
{
"path": "Amethyst/Model/ApplicationEventHandler.swift",
"chars": 3120,
"preview": "//\n// ApplicationEventHandler.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 2/28/23.\n// Copyright © 2023 Ian"
},
{
"path": "Amethyst/Model/ApplicationObservation.swift",
"chars": 12506,
"preview": "//\n// ApplicationObservation.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/21/19.\n// Copyright © 2019 Ian "
},
{
"path": "Amethyst/Model/CGInfo.swift",
"chars": 6172,
"preview": "//\n// CGInfo.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/29/19.\n// Copyright © 2019 Ian Ynda-Hummel. All"
},
{
"path": "Amethyst/Model/Change.swift",
"chars": 546,
"preview": "//\n// Change.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/29/19.\n// Copyright © 2019 Ian Ynda-Hummel. All"
},
{
"path": "Amethyst/Model/MouseState.swift",
"chars": 5007,
"preview": "//\n// MouseState.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/21/19.\n// Copyright © 2019 Ian Ynda-Hummel."
},
{
"path": "Amethyst/Model/Reliability.swift",
"chars": 568,
"preview": "//\n// Reliability.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 9/8/19.\n// Copyright © 2019 Ian Ynda-Hummel."
},
{
"path": "Amethyst/Model/Screen.swift",
"chars": 6308,
"preview": "//\n// Screen.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 9/14/19.\n// Copyright © 2019 Ian Ynda-Hummel. All"
},
{
"path": "Amethyst/Model/Space.swift",
"chars": 272,
"preview": "//\n// Space.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 9/9/19.\n// Copyright © 2019 Ian Ynda-Hummel. All r"
},
{
"path": "Amethyst/Model/Window.swift",
"chars": 12239,
"preview": "//\n// Window.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/10/19.\n// Copyright © 2019 Ian Ynda-Hummel. All"
},
{
"path": "Amethyst/Model/WindowsInformation.swift",
"chars": 6715,
"preview": "//\n// WindowsInformation.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright © 2016 Ian Ynda"
},
{
"path": "Amethyst/Preferences/DebugPreferencesViewController.swift",
"chars": 256,
"preview": "//\n// DebugPreferencesViewController.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/9/19.\n// Copyright © 20"
},
{
"path": "Amethyst/Preferences/DebugPreferencesViewController.xib",
"chars": 7055,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
},
{
"path": "Amethyst/Preferences/FloatingPreferencesViewController.swift",
"chars": 8837,
"preview": "//\n// GeneralPreferencesViewController.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright ©"
},
{
"path": "Amethyst/Preferences/FloatingPreferencesViewController.xib",
"chars": 45283,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
},
{
"path": "Amethyst/Preferences/GeneralPreferencesViewController.swift",
"chars": 274,
"preview": "//\n// GeneralPreferencesViewController.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright ©"
},
{
"path": "Amethyst/Preferences/GeneralPreferencesViewController.xib",
"chars": 40924,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
},
{
"path": "Amethyst/Preferences/LayoutsPreferencesViewController.swift",
"chars": 5022,
"preview": "//\n// LayoutsPreferencesViewController.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 3/1/20.\n// Copyright © "
},
{
"path": "Amethyst/Preferences/LayoutsPreferencesViewController.xib",
"chars": 21419,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
},
{
"path": "Amethyst/Preferences/MousePreferencesViewController.swift",
"chars": 238,
"preview": "//\n// MousePreferencesViewController.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 2/29/20.\n// Copyright © 2"
},
{
"path": "Amethyst/Preferences/MousePreferencesViewController.xib",
"chars": 6986,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
},
{
"path": "Amethyst/Preferences/ShortcutsPreferencesListItemView.swift",
"chars": 1277,
"preview": "//\n// ShortcutsPreferencesListItemView.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright ©"
},
{
"path": "Amethyst/Preferences/ShortcutsPreferencesViewController.swift",
"chars": 1506,
"preview": "//\n// ShortcutsPreferencesViewController.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright"
},
{
"path": "Amethyst/Preferences/ShortcutsPreferencesViewController.xib",
"chars": 9937,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
},
{
"path": "Amethyst/Preferences/UserConfiguration.swift",
"chars": 27438,
"preview": "//\n// UserConfiguration.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/8/16.\n// Copyright © 2016 Ian Ynda-H"
},
{
"path": "Amethyst/View/LayoutNameWindow.swift",
"chars": 2578,
"preview": "//\n// LayoutNameWindow.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright © 2016 Ian Ynda-H"
},
{
"path": "Amethyst/View/LayoutNameWindow.xib",
"chars": 5984,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
},
{
"path": "Amethyst/View/LayoutNameWindowController.swift",
"chars": 235,
"preview": "//\n// LayoutNameWIndowController.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 1/16/16.\n// Copyright © 2016 "
},
{
"path": "Amethyst/View/PreferencesWindow.swift",
"chars": 1786,
"preview": "//\n// PreferencesWindow.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 7/19/17.\n// Copyright © 2017 Ian Ynda-"
},
{
"path": "Amethyst/default.amethyst",
"chars": 4630,
"preview": "{\n \"layouts\": [\n \"tall\",\n \"wide\",\n \"fullscreen\",\n \"column\"\n ],\n \"mod1\": [\n \""
},
{
"path": "Amethyst/en.lproj/Credits.rtf",
"chars": 436,
"preview": "{\\rtf0\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\n{\\colortbl;\\red255\\green255\\blue255;}\n\\paperw9840\\paperh8400\n\\pard\\tx560\\tx11"
},
{
"path": "Amethyst/en.lproj/InfoPlist.strings",
"chars": 45,
"preview": "/* Localized versions of Info.plist keys */\n\n"
},
{
"path": "Amethyst/main.swift",
"chars": 1118,
"preview": "//\n// main.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 2/24/20.\n// Copyright © 2020 Ian Ynda-Hummel. All r"
},
{
"path": "Amethyst.xcodeproj/project.pbxproj",
"chars": 87395,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "Amethyst.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 153,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:Amethyst.xcodep"
},
{
"path": "Amethyst.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
"chars": 3892,
"preview": "{\n \"originHash\" : \"0bdfbd20a0602e0ca7fe7531ef1f5ef5df49b4edc989a6121656a1209da2ca9e\",\n \"pins\" : [\n {\n \"identit"
},
{
"path": "Amethyst.xcodeproj/xcshareddata/xcschemes/Amethyst Debug CLI.xcscheme",
"chars": 5425,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1500\"\n version = \"2.0\">\n <BuildAction\n "
},
{
"path": "Amethyst.xcodeproj/xcshareddata/xcschemes/Amethyst.xcscheme",
"chars": 4705,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1500\"\n version = \"1.7\">\n <BuildAction\n "
},
{
"path": "Amethyst.xctestplan",
"chars": 683,
"preview": "{\n \"configurations\" : [\n {\n \"id\" : \"3A3BA42F-5862-48E3-9505-9D3676A1BA59\",\n \"name\" : \"Standard\",\n \"op"
},
{
"path": "Amethyst.xcworkspace/contents.xcworkspacedata",
"chars": 226,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"group:Amethyst.xcode"
},
{
"path": "Amethyst.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Amethyst.xcworkspace/xcshareddata/swiftpm/Package.resolved",
"chars": 4145,
"preview": "{\n \"originHash\" : \"5c090c150afce6ddb786afef9aa71f0237da58318f433cf9eea579c374d84d51\",\n \"pins\" : [\n {\n \"identit"
},
{
"path": "AmethystTests/AmethystTests-Bridging-Header.h",
"chars": 1,
"preview": "\n"
},
{
"path": "AmethystTests/Helpers/FrameAssignmentVerification.swift",
"chars": 2313,
"preview": "//\n// FrameAssignmentVerification.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 9/21/19.\n// Copyright ©"
},
{
"path": "AmethystTests/Helpers/TestBundle.swift",
"chars": 433,
"preview": "//\n// TestBundle.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 7/8/21.\n// Copyright © 2021 Ian Ynda-Hum"
},
{
"path": "AmethystTests/Info.plist",
"chars": 733,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "AmethystTests/Model/CustomLayouts/extended.js",
"chars": 766,
"preview": "function layout() {\n return {\n name: \"Extended Tall\",\n extends: \"tall\",\n getFrameAssignments: (w"
},
{
"path": "AmethystTests/Model/CustomLayouts/fullscreen.js",
"chars": 229,
"preview": "function layout() {\n return {\n name: \"Fullscreen\",\n getFrameAssignments: (windows, screenFrame) => {\n "
},
{
"path": "AmethystTests/Model/CustomLayouts/null.js",
"chars": 98,
"preview": "function layout() {\n return {\n name: \"Null\",\n getFrameAssignments: null\n };\n}\n"
},
{
"path": "AmethystTests/Model/CustomLayouts/recommended-main-pane-ratio.js",
"chars": 1403,
"preview": "function layout() {\n return {\n name: \"Ratio\",\n initialState: {\n mainPaneRatio: 0.5\n }"
},
{
"path": "AmethystTests/Model/CustomLayouts/static-ratio-tall-native-commands.js",
"chars": 2085,
"preview": "function layout() {\n return {\n name: \"Static Ratio Tall with Native Commands\",\n initialState: {\n "
},
{
"path": "AmethystTests/Model/CustomLayouts/static-ratio-tall.js",
"chars": 2056,
"preview": "function layout() {\n return {\n name: \"Static Ratio Tall\",\n initialState: {\n mainPaneCount: 1"
},
{
"path": "AmethystTests/Model/CustomLayouts/subset.js",
"chars": 3622,
"preview": "function layout() {\n return {\n name: \"Subset\",\n initialState: {\n ids: []\n },\n "
},
{
"path": "AmethystTests/Model/CustomLayouts/undefined.js",
"chars": 68,
"preview": "function layout() {\n return {\n name: \"Undefined\"\n };\n}\n"
},
{
"path": "AmethystTests/Model/CustomLayouts/uniform-columns.js",
"chars": 655,
"preview": "function layout() {\n return {\n name: \"Uniform Columns\",\n getFrameAssignments: (windows, screenFrame) =>"
},
{
"path": "AmethystTests/Model/TestScreen.swift",
"chars": 1297,
"preview": "//\n// TestScreen.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 9/14/19.\n// Copyright © 2019 Ian Ynda-Hu"
},
{
"path": "AmethystTests/Model/TestWindow.swift",
"chars": 1936,
"preview": "//\n// TestWindow.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 9/14/19.\n// Copyright © 2019 Ian Ynda-Hu"
},
{
"path": "AmethystTests/Tests/Categories/SIWindow+AmethystTests.swift",
"chars": 1410,
"preview": "@testable import Amethyst\nimport Cocoa\nimport Nimble\nimport Quick\n\nclass SIWindowAmethystTests: QuickSpec {\n override"
},
{
"path": "AmethystTests/Tests/Configuration/UserConfigurationTests.swift",
"chars": 34402,
"preview": "//\n// UserConfigurationTests.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/15/16.\n// Copyright © 2016 Ian "
},
{
"path": "AmethystTests/Tests/Layout/BinarySpacePartitioningLayoutTests.swift",
"chars": 27538,
"preview": "//\n// BinarySpacePartitioningLayoutTests.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 5/29/16.\n// Copyright"
},
{
"path": "AmethystTests/Tests/Layout/ColumnLayoutTests.swift",
"chars": 11064,
"preview": "//\n// ColumnLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 9/18/19.\n// Copyright © 2019 Ian "
},
{
"path": "AmethystTests/Tests/Layout/CustomLayoutTests.swift",
"chars": 36335,
"preview": "//\n// CustomLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 7/8/21.\n// Copyright © 2021 Ian Y"
},
{
"path": "AmethystTests/Tests/Layout/FloatingLayoutTests.swift",
"chars": 1899,
"preview": "//\n// FloatingLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 9/21/19.\n// Copyright © 2019 Ia"
},
{
"path": "AmethystTests/Tests/Layout/FullscreenLayoutTests.swift",
"chars": 3427,
"preview": "//\n// FullscreenLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 9/14/19.\n// Copyright © 2019 "
},
{
"path": "AmethystTests/Tests/Layout/RowLayoutTests.swift",
"chars": 11253,
"preview": "//\n// RowLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 9/21/19.\n// Copyright © 2019 Ian Ynd"
},
{
"path": "AmethystTests/Tests/Layout/TallLayoutTests.swift",
"chars": 10713,
"preview": "//\n// TallLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 9/21/19.\n// Copyright © 2019 Ian Yn"
},
{
"path": "AmethystTests/Tests/Layout/TallRightLayoutTests.swift",
"chars": 10757,
"preview": "//\n// TallRightLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 12/18/19.\n// Copyright © 2019 "
},
{
"path": "AmethystTests/Tests/Layout/ThreeColumnLayoutTests.swift",
"chars": 35722,
"preview": "//\n// ThreeColumnLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 12/19/19.\n// Copyright © 201"
},
{
"path": "AmethystTests/Tests/Layout/TwoPaneLayoutTests.swift",
"chars": 17807,
"preview": "//\n// TwoPaneLayoutTests.swift\n// AmethystTests\n//\n// Created by @mwz on 14/06/21.\n// Copyright © 2021 Ian Ynda-Humm"
},
{
"path": "AmethystTests/Tests/Layout/WideLayoutTests.swift",
"chars": 10698,
"preview": "//\n// WideLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 12/7/19.\n// Copyright © 2019 Ian Yn"
},
{
"path": "AmethystTests/Tests/Layout/WidescreenTallLayoutTests.swift",
"chars": 18875,
"preview": "//\n// WidescreenTallLayoutTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 12/18/19.\n// Copyright © "
},
{
"path": "AmethystTests/Tests/Managers/HotKeyManagerTests.swift",
"chars": 677,
"preview": "//\n// HotKeyManagerTests.swift\n// Amethyst\n//\n// Created by Ian Ynda-Hummel on 4/18/17.\n// Copyright © 2017 Ian Ynda"
},
{
"path": "AmethystTests/Tests/Managers/ScreenManagerTests.swift",
"chars": 9978,
"preview": "//\n// ScreenManagerTests.swift\n// AmethystTests\n//\n// Created by Ian Ynda-Hummel on 2/11/20.\n// Copyright © 2020 Ian"
},
{
"path": "Brewfile",
"chars": 204,
"preview": "# run 'brew bundle' to install all listed packages\n\n# Build tools\nbrew \"fastlane\" # Easiest way to build and release mob"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3214,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "LICENSE.md",
"chars": 1059,
"preview": "Copyright (c) 2015 Ian Ynda-Hummel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis"
},
{
"path": "README.md",
"chars": 11655,
"preview": "# Amethyst\n\n[](https://github.com/ianyh/Amethyst"
},
{
"path": "docs/configuration-files.md",
"chars": 9158,
"preview": "# Configuration Files\n\nAmethyst will pick up a config file located at `~/.amethyst.yml` or `~/.config/amethyst/amethyst."
},
{
"path": "docs/custom-layouts.md",
"chars": 6079,
"preview": "# Custom Layouts (beta)\n\nAmethyst supports implementing custom layouts via JavaScript.\n\n## Installing\n\nLayouts are locat"
},
{
"path": "docs/troubleshooting.md",
"chars": 1163,
"preview": "# Troubleshooting\n\n## Nothing is working!\n\nHere are some common problems and their solutions.\n\n### \"Always float\"\n\nAmeth"
},
{
"path": "docs/window-limit.md",
"chars": 1323,
"preview": "# Window Limit \n\n## How To Enable\n\nIn the General tab of Amethyst's preferences, there is a Maximum Window Count field. "
},
{
"path": "exportOptions.plist",
"chars": 372,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "fastlane/Appfile",
"chars": 230,
"preview": "# app_identifier(\"[[APP_IDENTIFIER]]\") # The bundle identifier of your app\n# apple_id(\"[[APPLE_ID]]\") # Your Apple email"
},
{
"path": "fastlane/Fastfile",
"chars": 670,
"preview": "# This file contains the fastlane.tools configuration\n# You can find the documentation at https://docs.fastlane.tools\n#\n"
},
{
"path": "fastlane/Gymfile",
"chars": 80,
"preview": "archive_path(\"./build/Amethyst\")\nscheme(\"Amethyst\")\noutput_directory(\"./build\")\n"
},
{
"path": "fastlane/README.md",
"chars": 666,
"preview": "fastlane documentation\n----\n\n# Installation\n\nMake sure you have the latest version of the Xcode command line tools insta"
},
{
"path": "privacy-policy.md",
"chars": 153,
"preview": "Amethyst Privacy Policy\n-----------------------\n\nAmethyst does not collect any personal information and does not transmi"
}
]
About this extraction
This page contains the full source code of the ianyh/Amethyst GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 146 files (900.9 KB), approximately 216.0k tokens, and a symbol index with 9 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.