├── .current-version ├── .gitignore ├── .sourcery.yml ├── .travis.yml ├── FUNDING.yml ├── Gemfile ├── Gemfile.lock ├── Gray.entitlements ├── Gray.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Gray.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Images └── Screenshot.png ├── LICENSE.md ├── Localisations ├── LocalizedUtils.swift ├── en.lproj │ └── Localizable.strings ├── ja.lproj │ └── Localizable.strings ├── pt-BR.lproj │ └── Localizable.strings ├── sp.lproj │ └── Localizable.strings └── zh-Hans.lproj │ └── Localizable.strings ├── Podfile ├── Podfile.lock ├── README.md ├── Resources ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── Colors │ │ ├── Contents.json │ │ ├── Dark.colorset │ │ │ └── Contents.json │ │ └── Light.colorset │ │ │ └── Contents.json │ ├── Contents.json │ ├── Grid.imageset │ │ ├── Contents.json │ │ └── Grid.pdf │ ├── Icons │ │ ├── Contents.json │ │ ├── Dock Appearance.imageset │ │ │ ├── Contents.json │ │ │ ├── dark.png │ │ │ ├── dark@2x.png │ │ │ ├── light-1.png │ │ │ ├── light.png │ │ │ ├── light@2x-1.png │ │ │ └── light@2x.png │ │ ├── Dock and Menu Appearance.imageset │ │ │ ├── Contents.json │ │ │ ├── dark.png │ │ │ ├── dark@2x.png │ │ │ ├── light-1.png │ │ │ ├── light.png │ │ │ ├── light@2x-1.png │ │ │ └── light@2x.png │ │ ├── System Appearance.imageset │ │ │ ├── Contents.json │ │ │ ├── dark-1.png │ │ │ ├── dark.png │ │ │ ├── dark@2x-1.png │ │ │ ├── dark@2x.png │ │ │ ├── light.png │ │ │ └── light@2x-1.png │ │ └── Window Appearance.imageset │ │ │ ├── Contents.json │ │ │ ├── dark.png │ │ │ ├── dark@2x.png │ │ │ ├── light-1.png │ │ │ ├── light.png │ │ │ ├── light@2x-1.png │ │ │ └── light@2x-2.png │ └── List.imageset │ │ ├── Contents.json │ │ └── List.pdf ├── Base.lproj │ └── MainMenu.xib ├── Info.plist ├── en.lproj │ └── MainMenu.strings └── zh-Hans.lproj │ └── MainMenu.xib ├── Sources ├── Alerts │ └── AlertsController.swift ├── Application │ ├── AppDelegate.swift │ ├── DepedencyContainer.swift │ ├── LayoutFactory.swift │ └── Toolbar │ │ ├── SearchField.swift │ │ ├── SearchToolbarItem.swift │ │ ├── Toolbar.swift │ │ └── ViewToolbarItem.swift ├── Extensions │ └── UserDefaults.swift ├── Features │ ├── Applications │ │ ├── AppearanceAware.swift │ │ ├── Application.swift │ │ ├── ApplicationGridView.swift │ │ ├── ApplicationListView.swift │ │ ├── ApplicationsFeatureViewController.swift │ │ ├── ApplicationsLoadingViewController.swift │ │ ├── ApplicationsLogicController.swift │ │ └── Component.swift │ ├── Export │ │ └── ExportController.swift │ ├── Import │ │ └── ImportController.swift │ ├── Main │ │ └── MainContainerViewController.swift │ ├── SystemPreferences │ │ ├── SystemLogicController.swift │ │ ├── SystemPreference.swift │ │ ├── SystemPreferenceFeatureViewController.swift │ │ └── SystemPreferenceView.swift │ └── Views │ │ └── OpaqueView.swift ├── Shared │ └── CollectionViewHeader.swift ├── Utilities │ ├── IconController.swift │ └── Shell.swift └── Versions │ └── VersionController.swift └── Voodoo ├── Output ├── CollectionViewItemComponent-macOS.generated.swift ├── StatefulItem-macOS.generated.swift └── ViewControllerFactory-macOS.generated.swift ├── Protocols ├── CollectionViewItemComponent.swift └── StatefulItem.swift └── Templates ├── CollectionViewItemComponent-macOS.stencil ├── StatefulItem-macOS.stencil └── ViewControllerFactory-macOS.stencil /.current-version: -------------------------------------------------------------------------------- 1 | 0.17.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | Icon 6 | ._* 7 | .Spotlight-V100 8 | .Trashes 9 | 10 | # Xcode 11 | # 12 | build/ 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata 22 | *.xccheckout 23 | *.moved-aside 24 | DerivedData 25 | *.hmap 26 | *.ipa 27 | *.xcuserstate 28 | 29 | # CocoaPods 30 | Pods 31 | 32 | # Carthage 33 | Carthage 34 | 35 | # SPM 36 | .build/ 37 | 38 | -------------------------------------------------------------------------------- /.sourcery.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | - Voodoo/Protocols 3 | - Sources 4 | templates: 5 | - Voodoo/Templates 6 | output: 7 | Voodoo/Output 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode11 2 | language: objective-c 3 | 4 | before_install: 5 | - bundle install 6 | - pod repo update 7 | - pod install 8 | 9 | script: 10 | - set -o pipefail && xcodebuild -workspace Gray.xcworkspace -scheme "Gray" -sdk macosx clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED="NO" | xcpretty 11 | 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) 14 | 15 | notifications: 16 | email: false 17 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [zenangst] 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods', '~> 1.9.1' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.11) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.3) 11 | claide (1.0.2) 12 | cocoapods (1.6.0.beta.2) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.2, < 2.0) 15 | cocoapods-core (= 1.6.0.beta.2) 16 | cocoapods-deintegrate (>= 1.0.2, < 2.0) 17 | cocoapods-downloader (>= 1.2.2, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.3.1, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored2 (~> 3.1) 24 | escape (~> 0.0.4) 25 | fourflusher (~> 2.0.1) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.6.6) 28 | nap (~> 1.0) 29 | ruby-macho (~> 1.3, >= 1.3.1) 30 | xcodeproj (>= 1.7.0, < 2.0) 31 | cocoapods-core (1.6.0.beta.2) 32 | activesupport (>= 4.0.2, < 6) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.2) 36 | cocoapods-downloader (1.2.2) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.0.0) 41 | cocoapods-trunk (1.3.1) 42 | nap (>= 0.8, < 2.0) 43 | netrc (~> 0.11) 44 | cocoapods-try (1.1.0) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.1.4) 47 | escape (0.0.4) 48 | fourflusher (2.0.1) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.6.6) 55 | nanaimo (0.2.6) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | ruby-macho (1.3.1) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.5) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.7.0) 63 | CFPropertyList (>= 2.3.3, < 4.0) 64 | atomos (~> 0.1.3) 65 | claide (>= 1.0.2, < 2.0) 66 | colored2 (~> 3.1) 67 | nanaimo (~> 0.2.6) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | cocoapods (~> 1.6.0.beta.2) 74 | 75 | BUNDLED WITH 76 | 1.13.6 77 | -------------------------------------------------------------------------------- /Gray.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | com.zenangst.Gray 8 | 9 | com.apple.security.automation.apple-events 10 | 11 | com.apple.security.scripting-targets 12 | 13 | com.apple.dt.Xcode 14 | 15 | * 16 | 17 | 18 | com.apple.security.temporary-exception.apple-events 19 | com.apple.dt.Xcode 20 | 21 | 22 | -------------------------------------------------------------------------------- /Gray.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Gray.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Gray.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Gray.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Images/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Images/Screenshot.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Licensed under the **MIT** license 2 | 3 | > Copyright (c) 2018 Christoffer Winterkvist 4 | > 5 | > Permission is hereby granted, free of charge, to any person obtaining 6 | > a copy of this software and associated documentation files (the 7 | > "Software"), to deal in the Software without restriction, including 8 | > without limitation the rights to use, copy, modify, merge, publish, 9 | > distribute, sublicense, and/or sell copies of the Software, and to 10 | > permit persons to whom the Software is furnished to do so, subject to 11 | > the following conditions: 12 | > 13 | > The above copyright notice and this permission notice shall be 14 | > included in all copies or substantial portions of the Software. 15 | > 16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /Localisations/LocalizedUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedUtils.swift 3 | // Gray 4 | // 5 | // Created by Licardo on 2019/10/11. 6 | // Copyright © 2019 zenangst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | extension String { 11 | var localized: String { 12 | return NSLocalizedString(self, comment: self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Localisations/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Gray 4 | 5 | Created by Licardo on 2019/10/11. 6 | Copyright © 2019 zenangst. All rights reserved. 7 | */ 8 | 9 | "A new version is available." = "A new version is available."; 10 | "Version" = "Version"; 11 | "is available for download on GitHub." = "is available for download on GitHub."; 12 | "Open GitHub" = "Open GitHub"; 13 | "OK" = "OK"; 14 | "You’re up-to-date!" = "You’re up-to-date!"; 15 | "is currently the newest version available." = "is currently the newest version available."; 16 | "Reset" = "Reset"; 17 | "Applications" = "Applications"; 18 | "Additional privileges needed" = "Additional privileges needed"; 19 | "To be able to change the appearance of apps like Mail, Messages, Safari and Home, you need to grant permission Full Disk Access." = "To be able to change the appearance of apps like Mail, Messages, Safari and Home, you need to grant permission Full Disk Access."; 20 | "Open Security & Preferences" = "Open Security & Preferences"; 21 | "Dark appearance" = "Dark appearance"; 22 | "Light appearance" = "Light appearance"; 23 | "System appearance" = "System appearance"; 24 | "🔐 Locked" = "🔐 Locked"; 25 | "Loading..." = "Loading..."; 26 | "Importing..." = "Importing..."; 27 | "Search results:" = "Search results:"; 28 | "Importing" = "Importing"; 29 | "settings." = "settings."; 30 | "Loading" = "Loading"; 31 | "Enable Accessibility." = "Enable Accessibility."; 32 | "For this Gray to work properly, you will have to enable accessibility. You can do this by adding it under the Privacy-tab under Security & Privacy in System Preferences." = "For this Gray to work properly, you will have to enable accessibility. You can do this by adding it under the Privacy-tab under Security & Privacy in System Preferences."; 33 | "System Preferences" = "System Preferences"; 34 | "Preferences" = "Preferences"; 35 | "System" = "System"; 36 | -------------------------------------------------------------------------------- /Localisations/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Gray 4 | 5 | Created by Licardo on 2019/10/11. 6 | Copyright © 2019 zenangst. All rights reserved. 7 | */ 8 | 9 | "A new version is available." = "最新版があります。"; 10 | "Version" = "バージョン"; 11 | "is available for download on GitHub." = "が GitHub でダウンロードできます。"; 12 | "Open GitHub" = "GitHub を開く"; 13 | "OK" = "OK"; 14 | "You’re up-to-date!" = "最新版をお使いです!"; 15 | "is currently the newest version available." = "は最新版です。"; 16 | "Reset" = "初期化"; 17 | "Applications" = "アプリケーション"; 18 | "Additional privileges needed" = "権限が必要です"; 19 | "To be able to change the appearance of apps like Mail, Messages, Safari and Home, you need to grant permission Full Disk Access." = "メール、メッセージ、Safari、ホームなどのアプリの外観を変更するには、ディスクへのフルディスクアクセスを許可する必要があります。"; 20 | "Open Security & Preferences" = "セキュリティの設定を開く"; 21 | "Dark appearance" = "ダーク表示"; 22 | "Light appearance" = "ライト表示"; 23 | "System appearance" = "システムの表示"; 24 | "🔐 Locked" = "🔐 ロック"; 25 | "Loading..." = "読み込み中..."; 26 | "Importing..." = "インポート中..."; 27 | "Search results:" = "検索結果:"; 28 | "Importing" = "インポート中"; 29 | "settings." = "の設定"; 30 | "Loading" = "読み込み中"; 31 | "Enable Accessibility." = "アクセシビリティの有効化"; 32 | "For this Gray to work properly, you will have to enable accessibility. You can do this by adding it under the Privacy-tab under Security & Privacy in System Preferences." = "この Gray を正しく動作させるには、アクセシビリティを許可する必要があります。これはシステム環境設定の「セキュリティとプライバシー」の「プライバシー」パネルから追加することで可能です。"; 33 | "System Preferences" = "システムの設定"; 34 | "Preferences" = "設定"; 35 | "System" = "システム"; 36 | -------------------------------------------------------------------------------- /Localisations/pt-BR.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Gray 4 | 5 | Created by Licardo on 2019/10/11. 6 | Copyright © 2019 zenangst. All rights reserved. 7 | */ 8 | 9 | "A new version is available." = "Uma nova versão está disponível."; 10 | "Version" = "Versão"; 11 | "is available for download on GitHub." = "está disponível para baixar no GitHub."; 12 | "Open GitHub" = "Abrir GitHub"; 13 | "OK" = "OK"; 14 | "You’re up-to-date!" = "Já está atualizado!"; 15 | "is currently the newest version available." = "é atualmente a mais nova versão disponível."; 16 | "Reset" = "Resetar"; 17 | "Applications" = "Aplicativos"; 18 | "Additional privileges needed" = "São necessárias permissões adicionais"; 19 | "To be able to change the appearance of apps like Mail, Messages, Safari and Home, you need to grant permission Full Disk Access." = "Para poder alterar a aparência de aplicativos como Mail, Mensagens, Safari e Casa você deve conceder Acesso Completo ao disco."; 20 | "Open Security & Preferences" = "Abrir Segurança & Preferências"; 21 | "Dark appearance" = "Aparência escura"; 22 | "Light appearance" = "Aparência clara"; 23 | "System appearance" = "Aparência do sistema"; 24 | "🔐 Locked" = "🔐 Bloqueado"; 25 | "Loading..." = "Carregando..."; 26 | "Importing..." = "Importando..."; 27 | "Search results:" = "Resultados da pesquisa:"; 28 | "Importing" = "Importando"; 29 | "settings." = "configurações."; 30 | "Loading" = "Carregando"; 31 | "Enable Accessibility." = "Habilitar Acessibilidade."; 32 | "For this Gray to work properly, you will have to enable accessibility. You can do this by adding it under the Privacy-tab under Security & Privacy in System Preferences." = "Para que o Gray funcione corretamente, você terá que habilitar os recursos de acessibilidade. É só ativá-los na aba Privacidade da seção Segurança e Privacidade nas Preferências do Sistema."; 33 | "System Preferences" = "Preferências do Sistema"; 34 | "Preferences" = "Preferências"; 35 | "System" = "Sistema"; 36 | -------------------------------------------------------------------------------- /Localisations/sp.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Gray 4 | 5 | Created by Licardo on 2019/10/11. 6 | Copyright © 2019 zenangst. All rights reserved. 7 | */ 8 | 9 | "A new version is available." = "Está disponible una nueva versión."; 10 | "Version" = "Versión"; 11 | "is available for download on GitHub." = "está disponible para su descarga en GitHub."; 12 | "Open GitHub" = "Abrir GitHub"; 13 | "OK" = "OK"; 14 | "You’re up-to-date!" = "¡Está actualizado!"; 15 | "is currently the newest version available." = "es actualmente la versión más nueva disponible."; 16 | "Reset" = "Reiniciar"; 17 | "Applications" = "Aplicaciones"; 18 | "Additional privileges needed" = "Se necesitan privilegios adicionales"; 19 | "To be able to change the appearance of apps like Mail, Messages, Safari and Home, you need to grant permission Full Disk Access." = "Para poder cambiar la apariencia de aplicaciones tales como Mail, Messages, Safari e Inicio, debe otorgar permiso de Acceso completo al disco."; 20 | "Open Security & Preferences" = "Abrir Seguridad & Preferencias"; 21 | "Dark appearance" = "Apariencia oscura"; 22 | "Light appearance" = "Apariencia clara"; 23 | "System appearance" = "Apariencia del sistema"; 24 | "🔐 Locked" = "🔐 Bloqueado"; 25 | "Loading..." = "Cargando..."; 26 | "Importing..." = "Importando..."; 27 | "Search results:" = "Resultados de la búsqueda:"; 28 | "Importing" = "Importando"; 29 | "settings." = "configuraciones."; 30 | "Loading" = "Cargando"; 31 | "Enable Accessibility." = "Habilitar Accesibilidad."; 32 | "For this Gray to work properly, you will have to enable accessibility. You can do this by adding it under the Privacy-tab under Security & Privacy in System Preferences." = "Para que Gray funcione correctamente, tendrá que habilitar la función accesibilidad. Simplemente agréguela en la pestaña Privacidad bajo la sección Seguridad y Privacidad en las Preferencias del Sistema."; 33 | "System Preferences" = "Preferencias del Sistema"; 34 | "Preferences" = "Preferencias"; 35 | "System" = "Sistema"; 36 | -------------------------------------------------------------------------------- /Localisations/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Gray 4 | 5 | Created by Licardo on 2019/10/11. 6 | Copyright © 2019 zenangst. All rights reserved. 7 | */ 8 | 9 | "A new version is available." = "有新版本可用。"; 10 | "Version" = "版本"; 11 | "is available for download on GitHub." = "可在 GitHub 上下载。"; 12 | "Open GitHub" = "打开 GitHub"; 13 | "OK" = "好的"; 14 | "You’re up-to-date!" = "您是最新版本!"; 15 | "is currently the newest version available." = "是当前可用的最新版本。"; 16 | "Reset" = "重置"; 17 | "Applications" = "应用程序"; 18 | "Additional privileges needed" = "需要额外的权限"; 19 | "To be able to change the appearance of apps like Mail, Messages, Safari and Home, you need to grant permission Full Disk Access." = "为了能够更改”邮件“,”信息“,”Safari“和”家庭“等应用程序的外观,您需要授予“完全磁盘访问”权限。"; 20 | "Open Security & Preferences" = "打开安全性与隐私"; 21 | "Dark appearance" = "深色模式"; 22 | "Light appearance" = "浅色模式"; 23 | "System appearance" = "跟随系统"; 24 | "🔐 Locked" = "🔐 锁定"; 25 | "Loading..." = "正在加载..."; 26 | "Importing..." = "正在导入..."; 27 | "Search results:" = "搜索结果:"; 28 | "Importing" = "正在导入"; 29 | "settings." = "设置。"; 30 | "Loading" = "正在加载"; 31 | "Enable Accessibility." = "启用辅助功能。"; 32 | "For this Gray to work properly, you will have to enable accessibility. You can do this by adding it under the Privacy-tab under Security & Privacy in System Preferences." = "为了使 Gray 正常工作,您必须启用辅助功能。您可以通过在“系统偏好设置”中“安全性和隐私”下“隐私”选项卡下添加它来完成此操作。"; 33 | "System Preferences" = "系统偏好设定"; 34 | "Preferences" = "首选项"; 35 | "System" = "系统"; 36 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :macos, '10.14' 3 | 4 | # Frameworks 5 | pod 'Blueprints', git: 'https://github.com/zenangst/Blueprints.git', branch: 'feature/preferred-layout-attributes' 6 | pod 'Differific' 7 | pod 'Family' 8 | pod 'UserInterface' 9 | pod 'Sourcery' 10 | 11 | target 'Gray' 12 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Blueprints (0.13.1) 3 | - Differific (0.8.2) 4 | - Family (0.22.11) 5 | - Sourcery (0.17.0) 6 | - UserInterface (0.5.0) 7 | 8 | DEPENDENCIES: 9 | - Blueprints (from `https://github.com/zenangst/Blueprints.git`, branch `feature/preferred-layout-attributes`) 10 | - Differific 11 | - Family 12 | - Sourcery 13 | - UserInterface 14 | 15 | SPEC REPOS: 16 | https://github.com/CocoaPods/Specs.git: 17 | - Differific 18 | - Family 19 | - Sourcery 20 | - UserInterface 21 | 22 | EXTERNAL SOURCES: 23 | Blueprints: 24 | :branch: feature/preferred-layout-attributes 25 | :git: https://github.com/zenangst/Blueprints.git 26 | 27 | CHECKOUT OPTIONS: 28 | Blueprints: 29 | :commit: 576191d2b834805635824cc5f6db8ff3ea76b7ad 30 | :git: https://github.com/zenangst/Blueprints.git 31 | 32 | SPEC CHECKSUMS: 33 | Blueprints: ddd6ab1e455c98471aaef2fdd0b09b5f9b53af5b 34 | Differific: c3840b9e4d1ee2216b2c0054c270e4a616df9327 35 | Family: d380fa14141faf72359813338ee64ef93920f326 36 | Sourcery: 3ed61be7c8a1218fce349266139379dba477efe0 37 | UserInterface: 54e15db9aceaec2b9686d00f471adb15d2598ea3 38 | 39 | PODFILE CHECKSUM: ec46b4f2901d457ac7f8988c354b20dac9182b1a 40 | 41 | COCOAPODS: 1.10.1 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gray 2 | 3 |
4 | 5 | [![CI Status](https://travis-ci.com/zenangst/Gray.svg?branch=master)](https://travis-ci.com/zenangst/Gray) 6 | ![Swift](https://img.shields.io/badge/%20in-swift%204.2-orange.svg) 7 | [![macOS](https://img.shields.io/badge/macOS-10.14-green.svg)](https://www.apple.com/macos/mojave/) 8 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 9 | 10 |
11 | 12 | Gray Icon 13 | 14 | Current version: 0.17.0 [[Download](https://github.com/zenangst/Gray/releases/download/0.17.0/Gray.zip)] 15 | 16 | Ever wanted to have light and dark apps live side-by-side in harmony? Well, now you can. With **Gray** you can pick between the light appearance and the dark appearance on a per-app basis with the click of a button. 17 | 18 | To quote the late Michael Jackson: 19 | > It don't matter if you're black or white 20 | 21 | ### Instructions 22 | 23 | Go into `System Preferences > General` and set your Mac to use dark appearance. 24 | 25 | **Note** the application that you want to change the appearance of will have to restart before you see the changes. This is currently handled by **Gray** but be sure to not have any unsaved changes before you start tailoring your macOS experience. 26 | 27 | Gray 28 | 29 | ### How it works 30 | 31 | Under the hood, **Gray** simply configures which app should be forced to use the light aqua appearance. You can achieve this without installing **Gray** by merely running a terminal command. 32 | 33 | ```fish 34 | defaults write com.apple.dt.Xcode NSRequiresAquaSystemAppearance -bool YES 35 | ``` 36 | 37 | The command creates a new entry in the user's configuration file for the specific application. It does not alter the system in any way. So when you are done configuring, you can toss **Gray** in the trash if you like (I hope you don't :) ) 38 | 39 | ## Building 40 | 41 | If you want to build `Gray` using Xcode, you can follow these instructions. 42 | 43 | ```fish 44 | git clone git@github.com:zenangst/Gray.git 45 | cd Gray 46 | pod install 47 | open Gray.xcworkspace 48 | ``` 49 | 50 | Happy coding! 51 | 52 | ## Supporting the project 53 | 54 | If you want to support the development of this framework, you can do so by becoming a [sponsor](https://github.com/sponsors/zenangst). ❤️ 55 | 56 | ## Author 57 | 58 | Christoffer Winterkvist, christoffer@winterkvist.com 59 | 60 | ## License 61 | 62 | **Gray** is available under the MIT license. See the [LICENSE](https://github.com/zenangst/Gray/blob/master/LICENSE.md) file for more info. 63 | -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Colors/Dark.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties" : { 3 | "localizable" : true 4 | }, 5 | "info" : { 6 | "version" : 1, 7 | "author" : "xcode" 8 | }, 9 | "colors" : [ 10 | { 11 | "idiom" : "mac", 12 | "color" : { 13 | "color-space" : "srgb", 14 | "components" : { 15 | "red" : "0.141", 16 | "alpha" : "1.000", 17 | "blue" : "0.145", 18 | "green" : "0.145" 19 | } 20 | } 21 | }, 22 | { 23 | "idiom" : "mac", 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "color" : { 31 | "color-space" : "srgb", 32 | "components" : { 33 | "red" : "0.141", 34 | "alpha" : "1.000", 35 | "blue" : "0.145", 36 | "green" : "0.145" 37 | } 38 | } 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Colors/Light.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties" : { 3 | "localizable" : true 4 | }, 5 | "info" : { 6 | "version" : 1, 7 | "author" : "xcode" 8 | }, 9 | "colors" : [ 10 | { 11 | "idiom" : "mac", 12 | "color" : { 13 | "color-space" : "srgb", 14 | "components" : { 15 | "red" : "215", 16 | "alpha" : "1.000", 17 | "blue" : "215", 18 | "green" : "215" 19 | } 20 | } 21 | }, 22 | { 23 | "idiom" : "mac", 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "color" : { 31 | "color-space" : "srgb", 32 | "components" : { 33 | "red" : "255", 34 | "alpha" : "1.000", 35 | "blue" : "255", 36 | "green" : "255" 37 | } 38 | } 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Grid.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Grid.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template", 14 | "preserves-vector-representation" : true, 15 | "auto-scaling" : "auto" 16 | } 17 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Grid.imageset/Grid.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Grid.imageset/Grid.pdf -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock Appearance.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "light-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "light.png", 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "light" 15 | } 16 | ], 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "mac", 21 | "filename" : "dark.png", 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "dark" 26 | } 27 | ], 28 | "scale" : "1x" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "filename" : "light@2x-1.png", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "filename" : "light@2x.png", 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "light" 42 | } 43 | ], 44 | "scale" : "2x" 45 | }, 46 | { 47 | "idiom" : "mac", 48 | "filename" : "dark@2x.png", 49 | "appearances" : [ 50 | { 51 | "appearance" : "luminosity", 52 | "value" : "dark" 53 | } 54 | ], 55 | "scale" : "2x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock Appearance.imageset/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock Appearance.imageset/dark.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock Appearance.imageset/dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock Appearance.imageset/dark@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock Appearance.imageset/light-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock Appearance.imageset/light-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock Appearance.imageset/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock Appearance.imageset/light.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock Appearance.imageset/light@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock Appearance.imageset/light@2x-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock Appearance.imageset/light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock Appearance.imageset/light@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "light-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "light.png", 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "light" 15 | } 16 | ], 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "mac", 21 | "filename" : "dark.png", 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "dark" 26 | } 27 | ], 28 | "scale" : "1x" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "filename" : "light@2x.png", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "filename" : "light@2x-1.png", 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "light" 42 | } 43 | ], 44 | "scale" : "2x" 45 | }, 46 | { 47 | "idiom" : "mac", 48 | "filename" : "dark@2x.png", 49 | "appearances" : [ 50 | { 51 | "appearance" : "luminosity", 52 | "value" : "dark" 53 | } 54 | ], 55 | "scale" : "2x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/dark.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/dark@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/light-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/light-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/light.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/light@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/light@2x-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Dock and Menu Appearance.imageset/light@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/System Appearance.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "dark-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "dark.png", 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "light" 15 | } 16 | ], 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "mac", 21 | "filename" : "light.png", 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "dark" 26 | } 27 | ], 28 | "scale" : "1x" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "filename" : "dark@2x-1.png", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "filename" : "dark@2x.png", 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "light" 42 | } 43 | ], 44 | "scale" : "2x" 45 | }, 46 | { 47 | "idiom" : "mac", 48 | "filename" : "light@2x-1.png", 49 | "appearances" : [ 50 | { 51 | "appearance" : "luminosity", 52 | "value" : "dark" 53 | } 54 | ], 55 | "scale" : "2x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/System Appearance.imageset/dark-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/System Appearance.imageset/dark-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/System Appearance.imageset/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/System Appearance.imageset/dark.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/System Appearance.imageset/dark@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/System Appearance.imageset/dark@2x-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/System Appearance.imageset/dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/System Appearance.imageset/dark@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/System Appearance.imageset/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/System Appearance.imageset/light.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/System Appearance.imageset/light@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/System Appearance.imageset/light@2x-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Window Appearance.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "light.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "light-1.png", 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "light" 15 | } 16 | ], 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "mac", 21 | "filename" : "dark.png", 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "dark" 26 | } 27 | ], 28 | "scale" : "1x" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "filename" : "light@2x-1.png", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "filename" : "light@2x-2.png", 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "light" 42 | } 43 | ], 44 | "scale" : "2x" 45 | }, 46 | { 47 | "idiom" : "mac", 48 | "filename" : "dark@2x.png", 49 | "appearances" : [ 50 | { 51 | "appearance" : "luminosity", 52 | "value" : "dark" 53 | } 54 | ], 55 | "scale" : "2x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Window Appearance.imageset/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Window Appearance.imageset/dark.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Window Appearance.imageset/dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Window Appearance.imageset/dark@2x.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Window Appearance.imageset/light-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Window Appearance.imageset/light-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Window Appearance.imageset/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Window Appearance.imageset/light.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Window Appearance.imageset/light@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Window Appearance.imageset/light@2x-1.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/Icons/Window Appearance.imageset/light@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/Icons/Window Appearance.imageset/light@2x-2.png -------------------------------------------------------------------------------- /Resources/Assets.xcassets/List.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "List.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template", 14 | "preserves-vector-representation" : true, 15 | "auto-scaling" : "auto" 16 | } 17 | } -------------------------------------------------------------------------------- /Resources/Assets.xcassets/List.imageset/List.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zenangst/Gray/ec4fcb9e3daa73128c10b12c48453681cd87cd1e/Resources/Assets.xcassets/List.imageset/List.pdf -------------------------------------------------------------------------------- /Resources/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSAppleEventsUsageDescription 28 | Gray needs access to automation in order to perform the desired operations. 29 | NSHumanReadableCopyright 30 | Copyright © 2018 zenangst. All rights reserved. 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Resources/en.lproj/MainMenu.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSMenuItem"; title = "Check for Updates..."; ObjectID = "1Ic-d9-XXx"; */ 3 | "1Ic-d9-XXx.title" = "Check for Updates..."; 4 | 5 | /* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ 6 | "1UK-8n-QPP.title" = "Customize Toolbar…"; 7 | 8 | /* Class = "NSMenuItem"; title = "Gray"; ObjectID = "1Xt-HY-uBw"; */ 9 | "1Xt-HY-uBw.title" = "Gray"; 10 | 11 | /* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ 12 | "4J7-dP-txa.title" = "Enter Full Screen"; 13 | 14 | /* Class = "NSMenuItem"; title = "Quit Gray"; ObjectID = "4sb-4s-VLi"; */ 15 | "4sb-4s-VLi.title" = "Quit Gray"; 16 | 17 | /* Class = "NSMenuItem"; title = "About Gray"; ObjectID = "5kV-Vb-QxS"; */ 18 | "5kV-Vb-QxS.title" = "About Gray"; 19 | 20 | /* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ 21 | "AYu-sK-qS6.title" = "Main Menu"; 22 | 23 | /* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ 24 | "BOF-NM-1cW.title" = "Preferences…"; 25 | 26 | /* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ 27 | "F2S-fz-NVQ.title" = "Help"; 28 | 29 | /* Class = "NSMenuItem"; title = "Gray Help"; ObjectID = "FKE-Sm-Kum"; */ 30 | "FKE-Sm-Kum.title" = "Gray Help"; 31 | 32 | /* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ 33 | "H8h-7b-M4v.title" = "View"; 34 | 35 | /* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ 36 | "HyV-fh-RgO.title" = "View"; 37 | 38 | /* Class = "NSMenuItem"; title = "Import"; ObjectID = "KaW-ft-85H"; */ 39 | "KaW-ft-85H.title" = "Import"; 40 | 41 | /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ 42 | "Kd2-mp-pUS.title" = "Show All"; 43 | 44 | /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ 45 | "LE2-aR-0XJ.title" = "Bring All to Front"; 46 | 47 | /* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ 48 | "NMo-om-nkz.title" = "Services"; 49 | 50 | /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ 51 | "OY7-WF-poV.title" = "Minimize"; 52 | 53 | /* Class = "NSMenuItem"; title = "Hide Gray"; ObjectID = "Olw-nP-bQN"; */ 54 | "Olw-nP-bQN.title" = "Hide Gray"; 55 | 56 | /* Class = "NSMenuItem"; title = "as Grid"; ObjectID = "QN9-Xa-3J4"; */ 57 | "QN9-Xa-3J4.title" = "as Grid"; 58 | 59 | /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ 60 | "R4o-n2-Eq4.title" = "Zoom"; 61 | 62 | /* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ 63 | "Td7-aD-5lo.title" = "Window"; 64 | 65 | /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ 66 | "Vdr-fp-XzO.title" = "Hide Others"; 67 | 68 | /* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ 69 | "aUF-d1-5bR.title" = "Window"; 70 | 71 | /* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ 72 | "bib-Uj-vzu.title" = "File"; 73 | 74 | /* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ 75 | "dMs-cI-mzQ.title" = "File"; 76 | 77 | /* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ 78 | "hz9-B4-Xy5.title" = "Services"; 79 | 80 | /* Class = "NSMenuItem"; title = "as List"; ObjectID = "oUf-cU-ksv"; */ 81 | "oUf-cU-ksv.title" = "as List"; 82 | 83 | /* Class = "NSMenuItem"; title = "Export"; ObjectID = "pxx-59-PXV"; */ 84 | "pxx-59-PXV.title" = "Export"; 85 | 86 | /* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ 87 | "snW-S8-Cw5.title" = "Show Toolbar"; 88 | 89 | /* Class = "NSMenu"; title = "Gray"; ObjectID = "uQy-DD-JDr"; */ 90 | "uQy-DD-JDr.title" = "Gray"; 91 | 92 | /* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ 93 | "wpr-3q-Mcd.title" = "Help"; 94 | -------------------------------------------------------------------------------- /Resources/zh-Hans.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /Sources/Alerts/AlertsController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class AlertsController: VersionControllerDelegate { 4 | let versionController: VersionController 5 | 6 | init(versionController: VersionController) { 7 | self.versionController = versionController 8 | } 9 | 10 | func showNewVersionDialog(version: String, handler completion : (Bool)->Void) { 11 | let alert = NSAlert() 12 | alert.messageText = "A new version is available.".localized 13 | alert.informativeText = "Version".localized + " \(version) " + "is available for download on GitHub.".localized 14 | alert.alertStyle = .informational 15 | alert.addButton(withTitle: "Open GitHub".localized) 16 | alert.addButton(withTitle: "OK".localized) 17 | completion(alert.runModal() == .alertFirstButtonReturn) 18 | } 19 | 20 | func showNoNewUpdates() { 21 | let alert = NSAlert() 22 | alert.messageText = "You’re up-to-date!".localized 23 | alert.informativeText = "Gray \(versionController.currentVersion()) " + "is currently the newest version available.".localized 24 | alert.alertStyle = .informational 25 | alert.addButton(withTitle: "OK".localized) 26 | alert.runModal() 27 | } 28 | 29 | // MARK: - VersionControllerDelegate 30 | 31 | func versionController(_ controller: VersionController, foundNewVersion version: String) { 32 | showNewVersionDialog(version: version) { openGitHub in 33 | if openGitHub { 34 | let url = URL(string: "https://github.com/zenangst/Gray/releases/tag/\(version)")! 35 | NSWorkspace.shared.open(url) 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @NSApplicationMain 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | let exportController = ExportController() 6 | let importController = ImportController() 7 | weak var toolbar: Toolbar? 8 | weak var window: NSWindow? 9 | weak var mainContainerViewController: MainContainerViewController? 10 | lazy var alertsController = AlertsController(versionController: versionController) 11 | lazy var versionController = VersionController() 12 | 13 | func applicationDidFinishLaunching(_ aNotification: Notification) { 14 | #if DEBUG 15 | Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load() 16 | #endif 17 | versionController.delegate = alertsController 18 | loadApplication() 19 | checkForNewVersion(nil) 20 | } 21 | 22 | func applicationDidBecomeActive(_ notification: Notification) { 23 | guard window == nil else { return } 24 | loadApplication() 25 | } 26 | 27 | @objc private func loadApplication() { 28 | let previousFrame = self.window?.frame 29 | self.window?.close() 30 | self.window = nil 31 | 32 | let dependencyContainer = DependencyContainer() 33 | let contentViewController = MainContainerViewController(iconStore: dependencyContainer) 34 | let toolbar = Toolbar(identifier: .init("MainApplicationWindowToolbar")) 35 | toolbar.toolbarDelegate = contentViewController 36 | let windowSize = CGSize(width: 400, height: 640) 37 | let window = NSWindow(contentViewController: contentViewController) 38 | window.setFrameAutosaveName(NSWindow.FrameAutosaveName.init("MainApplicationWindow")) 39 | window.styleMask = [.closable, .miniaturizable, .resizable, .titled, 40 | .fullSizeContentView, .unifiedTitleAndToolbar] 41 | window.titleVisibility = .hidden 42 | window.toolbar = toolbar 43 | window.minSize = windowSize 44 | window.maxSize = CGSize(width: 790 * 2, height: 1280) 45 | 46 | if window.frame.size.width < windowSize.width || window.frame.size.width > window.maxSize.width { 47 | window.setFrame(NSRect.init(origin: .zero, size: windowSize), 48 | display: false) 49 | } 50 | 51 | if let previousFrame = previousFrame { 52 | window.setFrame(previousFrame, display: true) 53 | } 54 | 55 | window.resizeIncrements = .init(width: 120 + 10, height: 1) 56 | window.makeKeyAndOrderFront(nil) 57 | self.window = window 58 | self.toolbar = toolbar 59 | 60 | mainContainerViewController = contentViewController 61 | } 62 | 63 | // MARK: - Injection 64 | 65 | @objc func injected() { 66 | loadApplication() 67 | } 68 | 69 | // MARK: - Actions 70 | 71 | @IBAction func switchToGrid(_ sender: Any?) { 72 | guard let toolbar = window?.toolbar as? Toolbar else { return } 73 | (window?.contentViewController as? MainContainerViewController)?.toolbar(toolbar, 74 | didChangeMode: ApplicationsFeatureViewController.Mode.grid.rawValue) 75 | NotificationCenter.default.post(name: NSNotification.Name.init(rawValue: "featureViewControllerMode"), object: nil) 76 | } 77 | 78 | @IBAction func switchToList(_ sender: Any?) { 79 | guard let toolbar = window?.toolbar as? Toolbar else { return } 80 | (window?.contentViewController as? MainContainerViewController)?.toolbar(toolbar, 81 | didChangeMode: ApplicationsFeatureViewController.Mode.list.rawValue) 82 | NotificationCenter.default.post(name: NSNotification.Name.init(rawValue: "featureViewControllerMode"), object: nil) 83 | } 84 | 85 | @IBAction func exportAction(_ sender: Any?) { 86 | exportController.openDialog() 87 | } 88 | 89 | @IBAction func importAction(_ sender: Any?) { 90 | importController.delegate = mainContainerViewController 91 | importController.openDialog() 92 | } 93 | 94 | @IBAction func search(_ sender: Any?) { 95 | toolbar?.searchField?.becomeFirstResponder() 96 | } 97 | 98 | @IBAction func checkForNewVersion(_ sender: Any?) { 99 | versionController.checkForNewVersion { [weak self] foundNewVersion in 100 | guard let strongSelf = self else { return } 101 | switch foundNewVersion { 102 | case true: 103 | let version = strongSelf.versionController.currentVersion() 104 | strongSelf.alertsController.showNewVersionDialog(version: version) { openGitHub in 105 | if openGitHub { 106 | let url = URL(string: "https://github.com/zenangst/Gray/releases/tag/\(version)")! 107 | NSWorkspace.shared.open(url) 108 | } 109 | } 110 | case false: 111 | if sender != nil { 112 | strongSelf.alertsController.showNoNewUpdates() 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /Sources/Application/DepedencyContainer.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class DependencyContainer: IconStore { 4 | private let iconController = IconController() 5 | 6 | func loadIcon(for application: Application, then handler: @escaping (NSImage?) -> Void) { 7 | DispatchQueue.global(qos: .userInteractive).async { [weak self] in 8 | guard let strongSelf = self else { return } 9 | let image = strongSelf.iconController.icon(for: application) 10 | DispatchQueue.main.async { handler(image) } 11 | } 12 | } 13 | } 14 | 15 | protocol IconStore { 16 | func loadIcon(for application: Application, then handler: @escaping (NSImage?) -> Void) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Application/LayoutFactory.swift: -------------------------------------------------------------------------------- 1 | import Blueprints 2 | import Cocoa 3 | 4 | class LayoutFactory { 5 | func createGridLayout() -> VerticalBlueprintLayout { 6 | let layout = VerticalBlueprintLayout( 7 | itemSize: .init(width: 120, height: 120), 8 | minimumInteritemSpacing: 10, 9 | minimumLineSpacing: 10, 10 | sectionInset: .init(top: 0, left: 10, bottom: 20, right: 10), 11 | animator: DefaultLayoutAnimator(animation: .fade)) 12 | return layout 13 | } 14 | 15 | func createListLayout() -> VerticalBlueprintLayout { 16 | let layout = VerticalBlueprintLayout( 17 | itemsPerRow: 1.0, 18 | height: 50, 19 | minimumInteritemSpacing: 10, 20 | minimumLineSpacing: 10, 21 | sectionInset: .init(top: 0, left: 10, bottom: 20, right: 10), 22 | animator: DefaultLayoutAnimator(animation: .fade)) 23 | return layout 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Application/Toolbar/SearchField.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class CustomTextFieldCell: NSSearchFieldCell { 4 | private static let padding = CGSize(width: 5.0, height: 5.0) 5 | 6 | override init(textCell string: String) { 7 | super.init(textCell: string) 8 | isEditable = true 9 | isBezeled = false 10 | } 11 | 12 | required init(coder: NSCoder) { 13 | fatalError("init(coder:) has not been implemented") 14 | } 15 | 16 | override func searchButtonRect(forBounds rect: NSRect) -> NSRect { 17 | let insetRect = rect.insetBy(dx: -CustomTextFieldCell.padding.width, 18 | dy: CustomTextFieldCell.padding.height) 19 | return super.searchButtonRect(forBounds: insetRect) 20 | } 21 | 22 | override func edit(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, event: NSEvent?) { 23 | let insetRect = rect.insetBy(dx: CustomTextFieldCell.padding.width, 24 | dy: CustomTextFieldCell.padding.height) 25 | super.edit(withFrame: insetRect, in: controlView, editor: textObj, delegate: delegate, event: event) 26 | } 27 | 28 | override func select(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, start selStart: Int, length selLength: Int) { 29 | let insetRect = rect.insetBy(dx: 20, 30 | dy: CustomTextFieldCell.padding.height) 31 | super.select(withFrame: insetRect, in: controlView, editor: textObj, delegate: delegate, start: selStart, length: selLength) 32 | } 33 | 34 | override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) { 35 | let insetRect = cellFrame.insetBy(dx: CustomTextFieldCell.padding.width, 36 | dy: CustomTextFieldCell.padding.height) 37 | super.drawInterior(withFrame: insetRect, in: controlView) 38 | } 39 | } 40 | 41 | class SearchField: NSSearchField { 42 | override init(frame frameRect: NSRect) { 43 | super.init(frame: frameRect) 44 | font = NSFont.systemFont(ofSize: 15) 45 | drawsBackground = true 46 | cell = CustomTextFieldCell(textCell: "") 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Application/Toolbar/SearchToolbarItem.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class SearchToolbarItem: NSToolbarItem, NSSearchFieldDelegate { 4 | static var itemIdentifier: NSToolbarItem.Identifier = .init("Search") 5 | 6 | lazy var titleLabel = SearchField() 7 | lazy var customView = NSView() 8 | 9 | init(text: String) { 10 | super.init(itemIdentifier: SearchToolbarItem.itemIdentifier) 11 | titleLabel.sizeToFit() 12 | titleLabel.delegate = self 13 | customView.frame = titleLabel.frame 14 | customView.addSubview(titleLabel) 15 | view = customView 16 | minSize = .init(width: 175, height: 25) 17 | maxSize = .init(width: 175, height: 25) 18 | setupConstraints() 19 | } 20 | 21 | func setupConstraints() { 22 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 23 | titleLabel.centerXAnchor.constraint(equalTo: customView.centerXAnchor).isActive = true 24 | titleLabel.centerYAnchor.constraint(equalTo: customView.centerYAnchor).isActive = true 25 | titleLabel.widthAnchor.constraint(equalTo: customView.widthAnchor).isActive = true 26 | titleLabel.heightAnchor.constraint(equalToConstant: 25).isActive = true 27 | } 28 | 29 | func controlTextDidChange(_ obj: Notification) { 30 | _ = titleLabel.target?.perform(titleLabel.action, with: titleLabel) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Application/Toolbar/Toolbar.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | protocol ToolbarDelegate: class { 4 | func toolbar(_ toolbar: Toolbar, didSearchFor string: String) 5 | func toolbar(_ toolbar: Toolbar, didChangeMode mode: String) 6 | } 7 | 8 | class Toolbar: NSToolbar, NSToolbarDelegate, ViewToolbarItemDelegate { 9 | weak var toolbarDelegate: ToolbarDelegate? 10 | weak var searchField: SearchField? 11 | 12 | override init(identifier: NSToolbar.Identifier) { 13 | super.init(identifier: identifier) 14 | allowsUserCustomization = true 15 | showsBaselineSeparator = true 16 | delegate = self 17 | } 18 | 19 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 20 | return [ 21 | NSToolbarItem.Identifier.space, 22 | NSToolbarItem.Identifier.flexibleSpace, 23 | ViewToolbarItem.itemIdentifier, 24 | SearchToolbarItem.itemIdentifier 25 | ] 26 | } 27 | 28 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 29 | return [ 30 | ViewToolbarItem.itemIdentifier, 31 | NSToolbarItem.Identifier.flexibleSpace, 32 | SearchToolbarItem.itemIdentifier, 33 | ] 34 | } 35 | 36 | func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { 37 | switch itemIdentifier { 38 | case ViewToolbarItem.itemIdentifier: 39 | let viewToolbarItem = ViewToolbarItem() 40 | viewToolbarItem.delegate = self 41 | return viewToolbarItem 42 | case SearchToolbarItem.itemIdentifier: 43 | let searchToolbarItem = SearchToolbarItem(text: "") 44 | searchToolbarItem.titleLabel.target = self 45 | searchToolbarItem.titleLabel.action = #selector(search(_:)) 46 | searchField = searchToolbarItem.titleLabel 47 | return searchToolbarItem 48 | case NSToolbarItem.Identifier.flexibleSpace: 49 | return NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.flexibleSpace) 50 | default: 51 | return nil 52 | } 53 | } 54 | 55 | @objc func search(_ label: SearchField) { 56 | toolbarDelegate?.toolbar(self, didSearchFor: label.stringValue) 57 | } 58 | 59 | // MARK: - ViewToolbarItemDelegate 60 | 61 | func viewToolbarItem(_ toolbarItem: ViewToolbarItem, didChange mode: String) { 62 | toolbarDelegate?.toolbar(self, didChangeMode: mode) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Application/Toolbar/ViewToolbarItem.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | protocol ViewToolbarItemDelegate: class { 4 | func viewToolbarItem(_ toolbarItem: ViewToolbarItem, didChange mode: String) 5 | } 6 | 7 | class ViewToolbarItem: NSToolbarItem { 8 | static var itemIdentifier: NSToolbarItem.Identifier = .init("View") 9 | 10 | weak var delegate: ViewToolbarItemDelegate? 11 | 12 | lazy var segmentedControl = NSSegmentedControl.init(images: ApplicationsFeatureViewController.Mode.allCases.compactMap({ $0.image }), 13 | trackingMode: .selectOne, 14 | target: self, 15 | action: #selector(didChangeView(_:))) 16 | lazy var customView = NSView() 17 | 18 | init() { 19 | super.init(itemIdentifier: ViewToolbarItem.itemIdentifier) 20 | view = customView 21 | view?.addSubview(segmentedControl) 22 | minSize = .init(width: 80, height: 25) 23 | maxSize = .init(width: 80, height: 25) 24 | segmentedControl.setToolTip("Grid", forSegment: 0) 25 | segmentedControl.setTag(0, forSegment: 0) 26 | segmentedControl.setToolTip("List", forSegment: 1) 27 | segmentedControl.setTag(1, forSegment: 1) 28 | configureSegmentControl() 29 | setupConstraints() 30 | 31 | NotificationCenter.default.addObserver(self, selector: #selector(configureSegmentControl), 32 | name: NSNotification.Name.init(rawValue: "featureViewControllerMode"), object: nil) 33 | } 34 | 35 | @objc func configureSegmentControl() { 36 | if let mode = UserDefaults.standard.featureViewControllerMode { 37 | switch mode { 38 | case .grid: 39 | segmentedControl.selectSegment(withTag: 0) 40 | case .list: 41 | segmentedControl.selectSegment(withTag: 1) 42 | } 43 | } else { 44 | segmentedControl.selectSegment(withTag: 0) 45 | } 46 | } 47 | 48 | func setupConstraints() { 49 | segmentedControl.translatesAutoresizingMaskIntoConstraints = false 50 | segmentedControl.centerXAnchor.constraint(equalTo: customView.centerXAnchor).isActive = true 51 | segmentedControl.centerYAnchor.constraint(equalTo: customView.centerYAnchor).isActive = true 52 | segmentedControl.widthAnchor.constraint(equalTo: customView.widthAnchor).isActive = true 53 | segmentedControl.heightAnchor.constraint(equalToConstant: 25).isActive = true 54 | } 55 | 56 | @objc func didChangeView(_ segmentControl: NSSegmentedControl) { 57 | guard let label = segmentedControl.toolTip(forSegment: segmentedControl.indexOfSelectedItem) else { 58 | return 59 | } 60 | 61 | delegate?.viewToolbarItem(self, didChange: label) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Extensions/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UserDefaults { 4 | var featureViewControllerMode: ApplicationsFeatureViewController.Mode? { 5 | get { 6 | let rawValue = UserDefaults.standard.string(forKey: #function) ?? "" 7 | return ApplicationsFeatureViewController.Mode.init(rawValue: rawValue) 8 | } 9 | set { 10 | UserDefaults.standard.set(newValue?.rawValue, forKey: #function) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Features/Applications/AppearanceAware.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | protocol AppearanceAware { 4 | var titleLabel: NSTextField { get } 5 | var subtitleLabel: NSTextField { get } 6 | var view: NSView { get } 7 | func update(with appearance: Application.Appearance, duration: TimeInterval, then handler: (() -> Void)?) 8 | } 9 | 10 | extension AppearanceAware { 11 | func update(with appearance: Application.Appearance, duration: TimeInterval = 0, then handler: (() -> Void)? = nil) { 12 | if duration > 0 { 13 | NSAnimationContext.current.allowsImplicitAnimation = true 14 | NSAnimationContext.runAnimationGroup({ (context) in 15 | context.duration = duration 16 | switch appearance { 17 | case .dark: 18 | view.animator().layer?.backgroundColor = NSColor(named: "Dark")?.cgColor 19 | titleLabel.animator().textColor = .white 20 | subtitleLabel.animator().textColor = .controlAccentColor 21 | view.layer?.borderWidth = 0.0 22 | case .system: 23 | view.animator().layer?.backgroundColor = NSColor.gray.cgColor 24 | titleLabel.animator().textColor = .white 25 | subtitleLabel.animator().textColor = .lightGray 26 | view.layer?.borderWidth = 0.0 27 | case .light: 28 | view.animator().layer?.backgroundColor = .white 29 | titleLabel.animator().textColor = .black 30 | subtitleLabel.animator().textColor = .controlAccentColor 31 | view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor 32 | view.layer?.borderWidth = 0 33 | } 34 | }, completionHandler:{ 35 | handler?() 36 | }) 37 | } else { 38 | switch appearance { 39 | case .dark: 40 | view.layer?.backgroundColor = NSColor(named: "Dark")?.cgColor 41 | titleLabel.textColor = .white 42 | subtitleLabel.textColor = .controlAccentColor 43 | view.layer?.borderWidth = 0.0 44 | case .light: 45 | view.layer?.backgroundColor = NSColor(named: "Light")?.cgColor 46 | titleLabel.textColor = .black 47 | subtitleLabel.textColor = .controlAccentColor 48 | view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor 49 | view.layer?.borderWidth = 1.0 50 | case .system: 51 | switch view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) { 52 | case .darkAqua?: 53 | view.layer?.backgroundColor = NSColor(named: "Dark")?.cgColor 54 | titleLabel.textColor = .white 55 | subtitleLabel.textColor = .lightGray 56 | view.layer?.borderWidth = 0.0 57 | case .aqua?: 58 | view.layer?.backgroundColor = NSColor(named: "Light")?.cgColor 59 | titleLabel.textColor = .black 60 | subtitleLabel.textColor = .controlAccentColor 61 | view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor 62 | view.layer?.borderWidth = 1.0 63 | default: 64 | break 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Features/Applications/Application.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Application: Hashable { 4 | enum Appearance: String, Hashable, CaseIterable { 5 | case light = "Light" 6 | case dark = "Dark" 7 | case system = "System" 8 | } 9 | 10 | let bundleIdentifier: String 11 | let name: String 12 | let metadata: String 13 | let url: URL 14 | let preferencesUrl: URL 15 | let appearance: Appearance 16 | let restricted: Bool 17 | var localizedName: String? 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Features/Applications/ApplicationGridView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import UserInterface 3 | 4 | protocol ApplicationGridViewDelegate: class { 5 | func applicationView(_ view: ApplicationGridView, didResetApplication currentAppearance: Application.Appearance?) 6 | } 7 | 8 | // sourcery: let application = Application 9 | class ApplicationGridView: NSCollectionViewItem, CollectionViewItemComponent, AppearanceAware { 10 | lazy var baseView = OpaqueView() 11 | weak var delegate: ApplicationGridViewDelegate? 12 | 13 | // sourcery: currentAppearance = model.application.appearance 14 | var currentAppearance: Application.Appearance? 15 | 16 | // sourcery: $RawBinding = "iconStore.loadIcon(for: model.application) { image in view.iconView.image = image }" 17 | lazy var iconView = NSImageView() 18 | // sourcery: let title: String = "titleLabel.stringValue = model.application.localizedName ?? model.title" 19 | lazy var titleLabel = NSTextField() 20 | // sourcery: let subtitle: String = "subtitleLabel.stringValue = model.subtitle" 21 | lazy var subtitleLabel = NSTextField() 22 | 23 | override func loadView() { 24 | self.view = baseView 25 | baseView.wantsLayer = true 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | let menu = NSMenu() 32 | menu.addItem(NSMenuItem(title: "Reset".localized, action: #selector(resetApplication), keyEquivalent: "")) 33 | view.menu = menu 34 | 35 | view.layer?.backgroundColor = NSColor.white.cgColor 36 | view.layer?.cornerRadius = 20 37 | view.layer?.masksToBounds = true 38 | 39 | titleLabel.backgroundColor = .clear 40 | titleLabel.isBezeled = false 41 | titleLabel.isEditable = false 42 | titleLabel.maximumNumberOfLines = 2 43 | titleLabel.lineBreakMode = .byWordWrapping 44 | titleLabel.font = NSFont.boldSystemFont(ofSize: 13) 45 | 46 | subtitleLabel.backgroundColor = .clear 47 | subtitleLabel.isBezeled = false 48 | subtitleLabel.isEditable = false 49 | subtitleLabel.maximumNumberOfLines = 1 50 | subtitleLabel.font = NSFont.boldSystemFont(ofSize: 9) 51 | 52 | view.addSubviews(iconView, titleLabel, subtitleLabel) 53 | 54 | let margin: CGFloat = 14 55 | 56 | NSLayoutConstraint.constrain( 57 | iconView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12), 58 | iconView.topAnchor.constraint(equalTo: view.topAnchor, constant: margin), 59 | iconView.widthAnchor.constraint(equalToConstant: 28), 60 | iconView.heightAnchor.constraint(equalToConstant: 28), 61 | 62 | titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin), 63 | titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin), 64 | titleLabel.bottomAnchor.constraint(equalTo: subtitleLabel.topAnchor, constant: 0), 65 | 66 | subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin), 67 | subtitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin), 68 | subtitleLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -margin) 69 | ) 70 | } 71 | 72 | override func viewDidLayout() { 73 | super.viewDidLayout() 74 | 75 | guard let currentAppearance = currentAppearance else { return } 76 | update(with: currentAppearance) 77 | } 78 | 79 | @objc func resetApplication() { 80 | delegate?.applicationView(self, didResetApplication: currentAppearance) 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /Sources/Features/Applications/ApplicationListView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | protocol ApplicationListViewDelegate: class { 4 | func applicationView(_ view: ApplicationListView, didResetApplication currentAppearance: Application.Appearance?) 5 | } 6 | 7 | // sourcery: let application = Application 8 | class ApplicationListView: NSCollectionViewItem, CollectionViewItemComponent, AppearanceAware { 9 | let baseView = OpaqueView() 10 | weak var delegate: ApplicationListViewDelegate? 11 | 12 | // sourcery: currentAppearance = model.application.appearance 13 | var currentAppearance: Application.Appearance? 14 | 15 | // sourcery: $RawBinding = "iconStore.loadIcon(for: model.application) { image in view.iconView.image = image }" 16 | lazy var iconView: NSImageView = .init() 17 | // sourcery: let title: String = "titleLabel.stringValue = model.application.localizedName ?? model.title" 18 | lazy var titleLabel: NSTextField = .init() 19 | // sourcery: let subtitle: String = "subtitleLabel.stringValue = model.subtitle" 20 | lazy var subtitleLabel: NSTextField = .init() 21 | 22 | private var layoutConstraints = [NSLayoutConstraint]() 23 | 24 | override func loadView() { 25 | self.view = baseView 26 | self.view.wantsLayer = true 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | let menu = NSMenu() 33 | menu.addItem(NSMenuItem(title: "Reset".localized, action: #selector(resetApplication), keyEquivalent: "")) 34 | view.menu = menu 35 | 36 | let verticalStackView = NSStackView(views: [titleLabel, subtitleLabel]) 37 | verticalStackView.alignment = .leading 38 | verticalStackView.orientation = .vertical 39 | verticalStackView.spacing = 0 40 | let stackView = NSStackView(views: [iconView, verticalStackView]) 41 | stackView.orientation = .horizontal 42 | 43 | titleLabel.isEditable = false 44 | titleLabel.drawsBackground = false 45 | titleLabel.isBezeled = false 46 | titleLabel.font = NSFont.boldSystemFont(ofSize: 13) 47 | 48 | subtitleLabel.isEditable = false 49 | subtitleLabel.drawsBackground = false 50 | subtitleLabel.isBezeled = false 51 | 52 | stackView.translatesAutoresizingMaskIntoConstraints = false 53 | view.addSubview(stackView) 54 | view.layer?.cornerRadius = 4 55 | 56 | NSLayoutConstraint.deactivate(layoutConstraints) 57 | layoutConstraints = [ 58 | stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), 59 | stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), 60 | stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), 61 | stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8) 62 | ] 63 | NSLayoutConstraint.activate(layoutConstraints) 64 | } 65 | 66 | override func viewDidLayout() { 67 | super.viewDidLayout() 68 | 69 | guard let currentAppearance = currentAppearance else { return } 70 | update(with: currentAppearance) 71 | } 72 | 73 | @objc func resetApplication() { 74 | delegate?.applicationView(self, didResetApplication: currentAppearance) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Features/Applications/ApplicationsFeatureViewController.swift: -------------------------------------------------------------------------------- 1 | import Blueprints 2 | import Cocoa 3 | import UserInterface 4 | 5 | protocol ApplicationsFeatureViewControllerDelegate: class { 6 | func applicationViewController(_ controller: ApplicationsFeatureViewController, 7 | finishedLoading: Bool) 8 | func applicationViewController(_ controller: ApplicationsFeatureViewController, 9 | didLoad application: Application, 10 | offset: Int, 11 | total: Int) 12 | func applicationViewController(_ controller: ApplicationsFeatureViewController, 13 | toggleAppearance newAppearance: Application.Appearance, 14 | application: Application) 15 | } 16 | 17 | class ApplicationsFeatureViewController: NSViewController, NSCollectionViewDelegate, 18 | ApplicationGridViewDelegate, ApplicationsLogicControllerDelegate, ApplicationListViewDelegate { 19 | enum Mode: String, CaseIterable { 20 | case grid = "Grid" 21 | case list = "List" 22 | 23 | var image: NSImage { 24 | switch self { 25 | case .grid: 26 | return NSImage.init(named: "Grid")! 27 | case .list: 28 | return NSImage.init(named: "List")! 29 | } 30 | } 31 | } 32 | 33 | enum State { 34 | case loading(application: Application, offset: Int, total: Int) 35 | case view([Application]) 36 | } 37 | 38 | weak var delegate: ApplicationsFeatureViewControllerDelegate? 39 | let listComponent: ApplicationListViewController 40 | let gridComponent: ApplicationGridViewController 41 | let logicController = ApplicationsLogicController() 42 | let iconStore: IconStore 43 | var mode: Mode { 44 | didSet { 45 | switch mode { 46 | case .grid: 47 | self.component = gridComponent 48 | case .list: 49 | self.component = listComponent 50 | } 51 | self.view = component.view 52 | configureComponent() 53 | } 54 | } 55 | var component: Component 56 | var applicationCache = [Application]() 57 | var query: String = "" 58 | 59 | init(iconStore: IconStore, mode: Mode?, models: [Application] = []) { 60 | let layoutFactory = LayoutFactory() 61 | self.iconStore = iconStore 62 | self.mode = mode ?? .grid 63 | self.gridComponent = ApplicationGridViewController(title: "Applications".localized, 64 | layout: layoutFactory.createGridLayout(), 65 | iconStore: iconStore) 66 | self.listComponent = ApplicationListViewController(title: "Applications".localized, 67 | layout: layoutFactory.createListLayout(), 68 | iconStore: iconStore) 69 | 70 | switch self.mode { 71 | case .grid: 72 | self.component = gridComponent 73 | case .list: 74 | self.component = listComponent 75 | } 76 | 77 | super.init(nibName: nil, bundle: nil) 78 | } 79 | 80 | required init?(coder: NSCoder) { 81 | fatalError("init(coder:) has not been implemented") 82 | } 83 | 84 | override func loadView() { 85 | self.view = component.view 86 | } 87 | 88 | override func viewDidLoad() { 89 | super.viewDidLoad() 90 | logicController.delegate = self 91 | configureComponent() 92 | } 93 | 94 | override func viewDidAppear() { 95 | super.viewDidAppear() 96 | logicController.load() 97 | } 98 | 99 | func configureComponent() { 100 | component.collectionView.delegate = self 101 | component.collectionView.isSelectable = true 102 | component.collectionView.allowsMultipleSelection = false 103 | } 104 | 105 | func toggle(_ newAppearance: Application.Appearance, for model: Application) { 106 | logicController.toggleAppearance(newAppearance, for: model) 107 | } 108 | 109 | func performSearch(with string: String) { 110 | query = string.lowercased() 111 | let filtered: [Application] 112 | switch string.count { 113 | case 0: 114 | filtered = applicationCache 115 | default: 116 | filtered = applicationCache.filter({ 117 | return ($0.localizedName ?? $0.name).lowercased().contains(query) 118 | }) 119 | } 120 | 121 | switch mode { 122 | case .grid: 123 | gridComponent.reload(with: gridModels(from: filtered)) 124 | case .list: 125 | listComponent.reload(with: listModels(from: filtered)) 126 | } 127 | } 128 | 129 | private func listModels(from applications: [Application]) -> [ApplicationListViewModel] { 130 | return applications.compactMap({ 131 | ApplicationListViewModel(title: $0.name, subtitle: $0.metadata, application: $0) 132 | }) 133 | } 134 | 135 | private func gridModels(from applications: [Application]) -> [ApplicationGridViewModel] { 136 | return applications.compactMap({ 137 | ApplicationGridViewModel(title: $0.name, subtitle: $0.metadata, application: $0) 138 | }) 139 | } 140 | 141 | private func render(_ newState: State) { 142 | switch newState { 143 | case .loading(let model, let offset, let total): 144 | delegate?.applicationViewController(self, didLoad: model, offset: offset, total: total) 145 | case .view(let applications): 146 | delegate?.applicationViewController(self, finishedLoading: true) 147 | applicationCache = applications 148 | 149 | let completion = { [weak self] in 150 | guard let strongSelf = self else { return } 151 | strongSelf.performSearch(with: strongSelf.query) 152 | } 153 | 154 | gridComponent.reload(with: gridModels(from: applications), completion: completion) 155 | listComponent.reload(with: listModels(from: applications), completion: completion) 156 | } 157 | } 158 | 159 | private func showPermissionsDialog(for application: Application, handler completion : (Bool)->Void) { 160 | let alert = NSAlert() 161 | alert.messageText = "Additional privileges needed".localized 162 | alert.informativeText = "To be able to change the appearance of apps like Mail, Messages, Safari and Home, you need to grant permission Full Disk Access.".localized 163 | alert.alertStyle = .informational 164 | alert.addButton(withTitle: "Open Security & Preferences".localized) 165 | alert.addButton(withTitle: "OK".localized) 166 | completion(alert.runModal() == .alertFirstButtonReturn) 167 | } 168 | 169 | // MARK: - ApplicationsLogicControllerDelegate 170 | 171 | func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplication application: Application, offset: Int, total: Int) { 172 | render(.loading(application: application, offset: offset, total: total)) 173 | } 174 | 175 | func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplications applications: [Application]) { 176 | render(.view(applications)) 177 | } 178 | 179 | // MARK: - ApplicationGridViewDelegate 180 | 181 | func applicationView(_ view: ApplicationGridView, didResetApplication currentAppearance: Application.Appearance?) { 182 | guard let indexPath = component.collectionView.indexPath(for: view) else { return } 183 | let model = gridComponent.model(at: indexPath) 184 | toggle(.system, for: model.application) 185 | } 186 | 187 | // MARK: - ApplicationListViewDelegate 188 | 189 | func applicationView(_ view: ApplicationListView, didResetApplication currentAppearance: Application.Appearance?) { 190 | guard let indexPath = component.collectionView.indexPath(for: view) else { return } 191 | let model = gridComponent.model(at: indexPath) 192 | toggle(.system, for: model.application) 193 | } 194 | 195 | // MARK: - NSCollectionViewDelegate 196 | 197 | func collectionView(_ collectionView: NSCollectionView, willDisplay item: NSCollectionViewItem, forRepresentedObjectAt indexPath: IndexPath) { 198 | if let view = item as? ApplicationGridView { 199 | view.delegate = self 200 | } 201 | 202 | if let view = item as? ApplicationListView { 203 | view.delegate = self 204 | } 205 | } 206 | 207 | func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { 208 | guard let indexPath = indexPaths.first else { return } 209 | guard let item: NSCollectionViewItem = collectionView.item(at: indexPath) else { return } 210 | 211 | collectionView.deselectAll(nil) 212 | 213 | let restricted: Bool 214 | let application: Application 215 | let newAppearance: Application.Appearance 216 | 217 | if collectionView.item(at: indexPath) is ApplicationGridView { 218 | let model = gridComponent.model(at: indexPath) 219 | restricted = model.application.restricted 220 | application = model.application 221 | newAppearance = model.application.appearance == .light 222 | ? .dark 223 | : .light 224 | } else if collectionView.item(at: indexPath) is ApplicationListView { 225 | let model = listComponent.model(at: indexPath) 226 | restricted = model.application.restricted 227 | application = model.application 228 | newAppearance = model.application.appearance == .light 229 | ? .dark 230 | : .light 231 | } else { return } 232 | 233 | let duration: TimeInterval = 0.15 234 | 235 | NSAnimationContext.runAnimationGroup({ (context) in 236 | let scale: CGFloat = 0.8 237 | let scaleTransform = CGAffineTransform.init(scaleX: scale, y: scale) 238 | let (width, height) = (item.view.frame.width / 2, item.view.frame.height / 2) 239 | let moveTransform = CGAffineTransform.init(translationX: width - (width * scale), 240 | y: height - (height * scale)) 241 | let concatTransform = scaleTransform.concatenating(moveTransform) 242 | context.duration = duration 243 | context.allowsImplicitAnimation = true 244 | item.view.animator().layer?.setAffineTransform(concatTransform) 245 | }, completionHandler:{ 246 | NSAnimationContext.runAnimationGroup({ (context) in 247 | context.duration = duration 248 | context.allowsImplicitAnimation = true 249 | item.view.animator().layer?.setAffineTransform(.identity) 250 | }, completionHandler: { 251 | if restricted { 252 | self.showPermissionsDialog(for: application) { result in 253 | guard result else { return } 254 | let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles")! 255 | NSWorkspace.shared.open(url) 256 | } 257 | } else { 258 | (item as? AppearanceAware)?.update(with: newAppearance, duration: 0.5) { [weak self] in 259 | guard let strongSelf = self else { return } 260 | strongSelf.delegate?.applicationViewController(strongSelf, 261 | toggleAppearance: newAppearance, 262 | application: application) 263 | } 264 | } 265 | }) 266 | }) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /Sources/Features/Applications/ApplicationsLoadingViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import UserInterface 3 | 4 | class ApplicationsLoadingViewController: NSViewController { 5 | override func loadView() { view = baseView } 6 | lazy var baseView = NSView() 7 | lazy var textField = NSTextField() 8 | lazy var progress = NSProgressIndicator() 9 | 10 | private var layoutConstraints = [NSLayoutConstraint]() 11 | 12 | init(text: String) { 13 | super.init(nibName: nil, bundle: nil) 14 | self.textField.stringValue = text 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | let stackView = NSStackView() 25 | stackView.orientation = .vertical 26 | stackView.alignment = .centerX 27 | stackView.distribution = .fillProportionally 28 | stackView.addArrangedSubview(progress) 29 | 30 | NSLayoutConstraint.deactivate(layoutConstraints) 31 | layoutConstraints = NSLayoutConstraint.addAndPin(stackView, toView: view, 32 | insets: .init(top: 0, left: 20, bottom: 0, right: 20)) 33 | 34 | textField.maximumNumberOfLines = -1 35 | textField.alignment = .center 36 | textField.isBezeled = false 37 | textField.isBordered = false 38 | textField.isEditable = false 39 | textField.isSelectable = false 40 | textField.drawsBackground = false 41 | textField.font = NSFont.systemFont(ofSize: 15) 42 | progress.canDrawConcurrently = true 43 | progress.isIndeterminate = false 44 | progress.style = .bar 45 | progress.doubleValue = 0.0 46 | } 47 | 48 | func setText(_ text: String) { 49 | self.textField.stringValue = text 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Features/Applications/ApplicationsLogicController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Cocoa 3 | 4 | protocol ApplicationsLogicControllerDelegate: class { 5 | func applicationsLogicController(_ controller: ApplicationsLogicController, 6 | didLoadApplication application: Application, 7 | offset: Int, total: Int) 8 | func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplications applications: [Application]) 9 | } 10 | 11 | class ApplicationsLogicController { 12 | weak var delegate: ApplicationsLogicControllerDelegate? 13 | let queue = DispatchQueue(label: "ApplicationQueue", qos: .userInitiated) 14 | 15 | enum PlistKey: String { 16 | case bundleName = "CFBundleName" 17 | case executableName = "CFBundleExecutable" 18 | case iconFile = "CFBundleIconFile" 19 | case bundleIdentifier = "CFBundleIdentifier" 20 | case applicationIsAgent = "LSUIElement" 21 | case requiresAquaSystemAppearance = "NSRequiresAquaSystemAppearance" 22 | } 23 | 24 | func load() { 25 | queue.async { [weak self] in 26 | guard let strongSelf = self else { return } 27 | do { 28 | let excludedBundles = ["com.vmware.fusion"] 29 | var applicationUrls = [URL]() 30 | for path in try strongSelf.applicationLocations() { 31 | applicationUrls.append(contentsOf: strongSelf.recursiveParse(at: path)) 32 | } 33 | let applications = try strongSelf.parseApplicationUrls(applicationUrls, excludedBundles: excludedBundles) 34 | DispatchQueue.main.async { [weak self] in 35 | guard let strongSelf = self else { return } 36 | strongSelf.delegate?.applicationsLogicController(strongSelf, didLoadApplications: applications) 37 | } 38 | } catch {} 39 | } 40 | } 41 | 42 | func toggleAppearance(_ newAppearance: Application.Appearance, 43 | for application: Application) { 44 | queue.async { [weak self] in 45 | let shell = Shell() 46 | 47 | let runningApplication = NSRunningApplication.runningApplications(withBundleIdentifier: application.bundleIdentifier).first 48 | 49 | if !application.url.path.contains("CoreServices") { 50 | runningApplication?.terminate() 51 | } 52 | 53 | // The cfprefsd is killed for the current user to avoid plist caching. 54 | // PlistBuddy is used to set new values. 55 | // Defaults is invoked in order to renew the cache. 56 | // https://nethack.ch/2014/03/30/quick-tip-flush-os-x-mavericks-plist-file-cache/ 57 | let command: String 58 | switch newAppearance { 59 | case .light: 60 | command = """ 61 | defaults write \(application.bundleIdentifier) NSRequiresAquaSystemAppearance -bool true 62 | defaults write \(application.bundleIdentifier.lowercased()) NSRequiresAquaSystemAppearance -bool true 63 | defaults read \(application.bundleIdentifier) NSRequiresAquaSystemAppearance \(application.preferencesUrl.path) 64 | """ 65 | case .dark: 66 | command = """ 67 | defaults write \(application.bundleIdentifier) NSRequiresAquaSystemAppearance -bool false 68 | defaults write \(application.bundleIdentifier.lowercased()) NSRequiresAquaSystemAppearance -bool false 69 | defaults read \(application.bundleIdentifier) NSRequiresAquaSystemAppearance \(application.preferencesUrl.path) 70 | """ 71 | case .system: 72 | command = """ 73 | defaults delete \(application.bundleIdentifier) NSRequiresAquaSystemAppearance 74 | defaults delete \(application.bundleIdentifier.lowercased()) NSRequiresAquaSystemAppearance 75 | defaults read \(application.bundleIdentifier) NSRequiresAquaSystemAppearance \(application.preferencesUrl.path) 76 | """ 77 | } 78 | 79 | 80 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 81 | let output = shell.execute(command: command) 82 | NSLog("Gray: terminal output: (\(output))") 83 | self?.load() 84 | } 85 | 86 | if runningApplication != nil && !application.url.path.contains("CoreServices") { 87 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { 88 | NSWorkspace.shared.launchApplication(withBundleIdentifier: application.bundleIdentifier, 89 | options: [.withoutActivation], 90 | additionalEventParamDescriptor: nil, 91 | launchIdentifier: nil) 92 | } 93 | } else { 94 | let shell = Shell() 95 | shell.execute(command: "killall", arguments: ["-9", "\(application.name)"]) 96 | } 97 | 98 | NSLog("Gray: New settings for \(application.name) = \(newAppearance)") 99 | NSLog("Gray: Command: \(command)") 100 | } 101 | } 102 | 103 | private func applicationLocations() throws -> [URL] { 104 | var directories = [URL]() 105 | let applicationDirectory = try FileManager.default.url(for: .allApplicationsDirectory, 106 | in: .localDomainMask, 107 | appropriateFor: nil, 108 | create: false) 109 | let applicationDirectoryU = try FileManager.default.url(for: .applicationDirectory, 110 | in: .userDomainMask, 111 | appropriateFor: nil, 112 | create: false) 113 | let homeDirectory = FileManager.default.homeDirectoryForCurrentUser 114 | let applicationDirectoryD = URL(fileURLWithPath: "/Developer/Applications") 115 | let applicationDirectoryN = URL(fileURLWithPath: "/Network/Applications") 116 | let applicationDirectoryND = URL(fileURLWithPath: "/Network/Developer/Applications") 117 | let coreServicesDirectory = URL(fileURLWithPath: "/System/Library/CoreServices") 118 | let systemApplicationsDirectory = URL(fileURLWithPath: "/System/Applications") 119 | let applicationDirectoryS = URL(fileURLWithPath: "/Users/Shared/Applications") 120 | 121 | // macOS default Applications directories 122 | directories.append(applicationDirectory) // including macOS default paths ./Utilities & ./Demos 123 | directories.append(applicationDirectoryU) // including macOS default paths ./Utilities & ./Demos 124 | directories.append(homeDirectory.appendingPathComponent("Developer/Applications")) 125 | directories.append(applicationDirectoryD) 126 | directories.append(applicationDirectoryN) // including macOS default paths ./Utilities & ./Demos 127 | directories.append(applicationDirectoryND) 128 | 129 | // other non-default application directories 130 | directories.append(coreServicesDirectory) // Gray *hopefully* excludes any non-application bundles; this path will also include Finder, Stocks etc. and several miscellaneous system applications in subdirectory /System/Library/CoreServices/Applications 131 | directories.append(applicationDirectory.appendingPathComponent("Xcode.app/Contents/Applications")) 132 | directories.append(applicationDirectory.appendingPathComponent("Xcode.app/Contents/Developer/Applications")) 133 | directories.append(homeDirectory.appendingPathComponent("Library/Developer/Xcode/DerivedData")) // default location for subdirectories containing applications freshly built with Xcode 134 | directories.append(applicationDirectoryS) 135 | directories.append(systemApplicationsDirectory) 136 | 137 | return directories 138 | } 139 | 140 | private func recursiveParse(at url: URL) -> [URL] { 141 | var result = [URL]() 142 | guard FileManager.default.fileExists(atPath: url.path), 143 | let contents = try? FileManager.default.contentsOfDirectory(at: url, 144 | includingPropertiesForKeys: nil, 145 | options: .skipsHiddenFiles) else { return [] } 146 | for file in contents { 147 | var isDirectory: ObjCBool = true 148 | let isFolder = FileManager.default.fileExists(atPath: file.path, isDirectory: &isDirectory) 149 | if isFolder && file.pathExtension != "app" && url.path.contains("/Applications") { 150 | result.append(contentsOf: recursiveParse(at: file)) 151 | } else { 152 | result.append(file) 153 | } 154 | } 155 | 156 | return result 157 | } 158 | 159 | private func parseApplicationUrls(_ appUrls: [URL], 160 | excludedBundles: [String] = []) throws -> [Application] { 161 | var applications = [Application]() 162 | let shell = Shell() 163 | let sip = shell.execute(command: "csrutil status").contains("enabled") 164 | let libraryDirectory = try FileManager.default.url(for: .libraryDirectory, 165 | in: .userDomainMask, 166 | appropriateFor: nil, 167 | create: false) 168 | var addedApplicationNames = [String]() 169 | let total = appUrls.count 170 | for (offset, url) in appUrls.enumerated() { 171 | let path = url.path 172 | let infoPath = "\(path)/Contents/Info.plist" 173 | guard FileManager.default.fileExists(atPath: infoPath), 174 | let plist = NSDictionary(contentsOfFile: infoPath), 175 | let bundleIdentifier = plist.value(forPlistKey: .bundleIdentifier), 176 | let bundleName = plist.value(forPlistKey: .bundleName) ?? plist.value(forPlistKey: .executableName), 177 | let executableName = plist.value(forPlistKey: .executableName), 178 | !addedApplicationNames.contains(bundleName), 179 | !excludedBundles.contains(bundleIdentifier) else { continue } 180 | 181 | if shouldExcludeApplication(with: plist, applicationUrl: url) == true { 182 | continue 183 | } 184 | 185 | let suffix = "Preferences/\(bundleIdentifier).plist" 186 | let appPreferenceUrl = libraryDirectory.appendingPathComponent(suffix) 187 | let appContainerPreferenceUrl = libraryDirectory.appendingPathComponent("Containers/\(bundleIdentifier)/Data/Library/\(suffix)") 188 | var resolvedAppPreferenceUrl = appPreferenceUrl 189 | var applicationPlist: NSDictionary? = nil 190 | 191 | if FileManager.default.fileExists(atPath: appContainerPreferenceUrl.path), 192 | let plist = NSDictionary.init(contentsOfFile: appContainerPreferenceUrl.path) { 193 | applicationPlist = plist 194 | resolvedAppPreferenceUrl = appContainerPreferenceUrl 195 | } else if let plist = NSDictionary.init(contentsOfFile: appPreferenceUrl.path) { 196 | applicationPlist = plist 197 | } 198 | 199 | // Check if Gray has enough priviliges to change appearance for application 200 | let restricted = sip && 201 | FileManager.default.fileExists(atPath: appContainerPreferenceUrl.path) && 202 | NSDictionary.init(contentsOfFile: appContainerPreferenceUrl.path) == nil 203 | 204 | let appearance = applicationPlist?.appearance() ?? .system 205 | var metadata: String 206 | switch appearance { 207 | case .dark: 208 | metadata = "Dark appearance".localized 209 | case .light: 210 | metadata = "Light appearance".localized 211 | case .system: 212 | metadata = "System appearance".localized 213 | } 214 | 215 | if restricted { 216 | metadata = "🔐 Locked".localized 217 | } 218 | 219 | var application = Application(bundleIdentifier: bundleIdentifier, 220 | name: bundleName, metadata: metadata, 221 | url: url, 222 | preferencesUrl: resolvedAppPreferenceUrl, 223 | appearance: appearance, 224 | restricted: restricted) 225 | 226 | application.localizedName = FileManager.default.displayName(atPath: application.url.path) 227 | 228 | DispatchQueue.main.async { [weak self] in 229 | guard let strongSelf = self else { return } 230 | strongSelf.delegate?.applicationsLogicController(strongSelf, didLoadApplication: application, offset: offset, total: total) 231 | } 232 | 233 | applications.append(application) 234 | addedApplicationNames.append(executableName) 235 | } 236 | return applications.sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) 237 | } 238 | 239 | private func shouldExcludeApplication(with plist: NSDictionary, applicationUrl url: URL) -> Bool { 240 | var shouldExcludeOnKeyword: Bool = false 241 | // Exclude applications with certain keywords in their name. 242 | let excludeKeywords = [ 243 | "handler", "agent", "migration", 244 | "problem", "setup", "uiserver", 245 | "install", "system image", "escrow"] 246 | 247 | for keyword in excludeKeywords { 248 | if url.lastPathComponent.lowercased().contains(keyword) { 249 | shouldExcludeOnKeyword = true 250 | break 251 | } 252 | } 253 | 254 | if shouldExcludeOnKeyword { 255 | return true 256 | } 257 | 258 | // Exclude applications that don't have an icon file. 259 | if plist.value(forPlistKey: .iconFile) == nil && url.path.contains("CoreServices") { 260 | return true 261 | } 262 | 263 | return false 264 | } 265 | } 266 | 267 | fileprivate extension NSDictionary { 268 | func appearance() -> Application.Appearance { 269 | let key = ApplicationsLogicController.PlistKey.requiresAquaSystemAppearance.rawValue 270 | if let result = (value(forKey: key) as? Bool) { 271 | return result ? .light : .dark 272 | } else { 273 | return .system 274 | } 275 | } 276 | 277 | func value(forPlistKey plistKey: ApplicationsLogicController.PlistKey) -> String? { 278 | return value(forKey: plistKey.rawValue) as? String 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /Sources/Features/Applications/Component.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | protocol Component { 4 | var collectionView: NSCollectionView { get } 5 | var view: NSView { get } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Features/Export/ExportController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class ExportController: NSObject, 4 | NSOpenSavePanelDelegate, 5 | ApplicationsLogicControllerDelegate { 6 | lazy var logicController = ApplicationsLogicController() 7 | lazy var panel = NSSavePanel() 8 | var destination: URL? 9 | var filename: String? 10 | 11 | // MARK: - Public methods 12 | 13 | func openDialog() { 14 | logicController.delegate = self 15 | let panel = NSSavePanel() 16 | panel.nameFieldStringValue = "gray-settings.txt" 17 | panel.delegate = self 18 | destination = panel.url 19 | 20 | panel.begin { response in 21 | if response == NSApplication.ModalResponse.OK { 22 | self.logicController.load() 23 | } 24 | } 25 | } 26 | 27 | // MARK: - NSOpenSavePanelDelegate 28 | 29 | func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String? { 30 | self.filename = filename 31 | return filename 32 | } 33 | 34 | func panel(_ sender: Any, didChangeToDirectoryURL url: URL?) { 35 | self.destination = url 36 | } 37 | 38 | // MARK: - ApplicationsLogicController 39 | 40 | func applicationsLogicController(_ controller: ApplicationsLogicController, 41 | didLoadApplication application: Application, 42 | offset: Int, 43 | total: Int) {} 44 | 45 | func applicationsLogicController(_ controller: ApplicationsLogicController, 46 | didLoadApplications applications: [Application]) { 47 | guard let destination = destination, 48 | let filename = filename else { return } 49 | 50 | var path = destination.absoluteString.replacingOccurrences(of: filename, with: "") 51 | path += filename 52 | 53 | guard let saveDestination = URL(string: path) else { return } 54 | 55 | var output = "" 56 | for application in applications where application.appearance != .system { 57 | 58 | let booleanString = application.appearance == .light 59 | ? "true" 60 | : "false" 61 | 62 | let command = """ 63 | defaults write \(application.bundleIdentifier) NSRequiresAquaSystemAppearance -bool \(booleanString)\n 64 | """ 65 | output += command 66 | } 67 | 68 | Swift.print("Write file to: \(saveDestination)") 69 | 70 | do { 71 | try (output as NSString).write(to: saveDestination, 72 | atomically: true, 73 | encoding: String.Encoding.utf8.rawValue) 74 | } catch let error { 75 | debugPrint(error) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Features/Import/ImportController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | enum ImportError: Error { 4 | case missingUrl 5 | } 6 | 7 | protocol ImportControllerDelegate: class { 8 | func importController(_ controller: ImportController, 9 | didStartImport: Bool, 10 | settingsCount: Int) 11 | func importController(_ controller: ImportController, 12 | offset: Int, 13 | importProgress progress: Double, 14 | settingsCount: Int) 15 | func importController(_ controller: ImportController, 16 | didFinishImport: Bool, 17 | settingsCount: Int) 18 | } 19 | 20 | class ImportController: NSObject { 21 | weak var delegate: ImportControllerDelegate? 22 | lazy var logicController = ApplicationsLogicController() 23 | var openPanel: NSOpenPanel? 24 | 25 | func openDialog() { 26 | let panel = NSOpenPanel() 27 | panel.canChooseDirectories = false 28 | panel.allowedFileTypes = ["txt"] 29 | panel.allowsMultipleSelection = false 30 | panel.begin(completionHandler: { try? self.handleDialogResponse($0) }) 31 | openPanel = panel 32 | } 33 | 34 | func handleDialogResponse(_ response: NSApplication.ModalResponse) throws { 35 | guard response == NSApplication.ModalResponse.OK, 36 | let destination = openPanel?.urls.first else { 37 | throw ImportError.missingUrl 38 | } 39 | defer { openPanel = nil } 40 | do { 41 | try self.validateAndImport(at: destination, handler: String.init) 42 | } catch let error { 43 | debugPrint(error) 44 | } 45 | } 46 | 47 | func validateAndImport(at url: URL, handler: (URL) throws -> String) rethrows { 48 | let contents = try handler(url) 49 | 50 | DispatchQueue.global(qos: .utility).async { [weak self] in 51 | guard let strongSelf = self else { return } 52 | 53 | let commands = contents.split(separator: "\n").compactMap(String.init) 54 | let shell = Shell() 55 | 56 | let validCommands = commands.filter(strongSelf.validateCommand) 57 | 58 | DispatchQueue.main.async { 59 | strongSelf.delegate?.importController(strongSelf, 60 | didStartImport: true, 61 | settingsCount: validCommands.count) 62 | } 63 | 64 | let total = Double(validCommands.count) 65 | 66 | for (offset, command) in validCommands.enumerated() { 67 | let output = shell.execute(command: command) 68 | if output.isEmpty { 69 | let progress = Double(offset + 1) / Double(total) * Double(100) 70 | DispatchQueue.main.async { 71 | strongSelf.delegate?.importController(strongSelf, 72 | offset: offset + 1, 73 | importProgress: progress, 74 | settingsCount: validCommands.count) 75 | } 76 | Swift.print("✅ \(command)") 77 | } else { 78 | Swift.print("❌ \(command)") 79 | } 80 | } 81 | 82 | DispatchQueue.main.async { 83 | strongSelf.delegate?.importController(strongSelf, 84 | didFinishImport: true, 85 | settingsCount: validCommands.count) 86 | } 87 | } 88 | } 89 | 90 | private func validateCommand(_ string: String) -> Bool { 91 | let words = string.split(separator: " ").compactMap(String.init) 92 | 93 | guard words.count == 6 else { return false } 94 | guard words[0] == "defaults" else { return false } 95 | guard words[1] == "write" else { return false } 96 | guard words[3] == "NSRequiresAquaSystemAppearance" else { return false } 97 | guard words[4] == "-bool" else { return false } 98 | guard ["true", "false"].contains(words[5]) else { return false } 99 | 100 | return true 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/Features/Main/MainContainerViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Family 3 | 4 | class MainContainerViewController: FamilyViewController, 5 | ApplicationsFeatureViewControllerDelegate, 6 | SystemPreferenceFeatureViewControllerDelegate, 7 | ToolbarDelegate, 8 | ImportControllerDelegate { 9 | 10 | lazy var loadingLabelController = ApplicationsLoadingViewController(text: "Loading...".localized) 11 | lazy var importLabelController = ApplicationsLoadingViewController(text: "Importing...".localized) 12 | let preferencesViewController: SystemPreferenceFeatureViewController 13 | let applicationsViewController: ApplicationsFeatureViewController 14 | let applicationLogicController = ApplicationsLogicController() 15 | 16 | init(iconStore: IconStore) { 17 | self.preferencesViewController = SystemPreferenceFeatureViewController(iconStore: iconStore) 18 | self.applicationsViewController = ApplicationsFeatureViewController(iconStore: iconStore, 19 | mode: UserDefaults.standard.featureViewControllerMode) 20 | super.init(nibName: nil, bundle: nil) 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | override func viewWillAppear() { 28 | super.viewWillAppear() 29 | children.forEach { $0.removeFromParent() } 30 | title = "Gray" 31 | 32 | applicationsViewController.delegate = self 33 | preferencesViewController.delegate = self 34 | 35 | body { 36 | add(importLabelController) 37 | add(loadingLabelController) 38 | add(preferencesViewController) 39 | add(applicationsViewController) 40 | loadingLabelController.view.frame.size.height = 120 41 | } 42 | 43 | loadingLabelController.view.enclosingScrollView?.drawsBackground = true 44 | } 45 | 46 | private func performSearch(with string: String) { 47 | let header = applicationsViewController.component.collectionView.supplementaryView(forElementKind: NSCollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? CollectionViewHeader 48 | 49 | body { 50 | switch string.count > 0 { 51 | case false: 52 | preferencesViewController.component.collectionView.animator().alphaValue = 1.0 53 | applicationsViewController.performSearch(with: string) 54 | header?.setText("Applications".localized) 55 | case true: 56 | preferencesViewController.component.collectionView.animator().alphaValue = 0.0 57 | applicationsViewController.performSearch(with: string) 58 | header?.setText("Search results:".localized + " \(string)") 59 | } 60 | } 61 | } 62 | 63 | // MARK: - ImportControllerDelegate 64 | 65 | func importController(_ controller: ImportController, didStartImport: Bool, settingsCount: Int) { 66 | importLabelController.view.alphaValue = 0.0 67 | body { 68 | importLabelController.view.frame.size.height = 75 69 | importLabelController.view.animator().alphaValue = 1.0 70 | } 71 | } 72 | 73 | func importController(_ controller: ImportController, 74 | offset: Int, 75 | importProgress progress: Double, 76 | settingsCount: Int) { 77 | importLabelController.progress.animator().doubleValue = floor(progress) 78 | importLabelController.textField.stringValue = "Importing".localized + " (\(offset)/\(settingsCount)) " + "settings.".localized 79 | } 80 | 81 | func importController(_ controller: ImportController, didFinishImport: Bool, settingsCount: Int) { 82 | body { 83 | importLabelController.view.animator().alphaValue = 0.0 84 | } 85 | applicationsViewController.logicController.load() 86 | } 87 | 88 | // MARK: - ToolbarSearchDelegate 89 | 90 | func toolbar(_ toolbar: Toolbar, didSearchFor string: String) { 91 | performSearch(with: string) 92 | } 93 | 94 | func toolbar(_ toolbar: Toolbar, didChangeMode mode: String) { 95 | guard let mode = ApplicationsFeatureViewController.Mode.init(rawValue: mode) else { 96 | return 97 | } 98 | UserDefaults.standard.featureViewControllerMode = mode 99 | applicationsViewController.mode = mode 100 | applicationsViewController.removeFromParent() 101 | add(applicationsViewController) 102 | } 103 | 104 | // MARK: - ApplicationCollectionViewControllerDelegate 105 | 106 | func applicationViewController(_ controller: ApplicationsFeatureViewController, finishedLoading: Bool) { 107 | loadingLabelController.view.alphaValue = 0.0 108 | } 109 | 110 | func applicationViewController(_ controller: ApplicationsFeatureViewController, 111 | didLoad application: Application, offset: Int, total: Int) { 112 | let progress = Double(offset + 1) / Double(total) * Double(100) 113 | loadingLabelController.progress.doubleValue = floor(progress) 114 | loadingLabelController.textField.stringValue = "Loading".localized + " (\(offset)/\(total)): \(application.name)" 115 | } 116 | 117 | func applicationViewController(_ controller: ApplicationsFeatureViewController, 118 | toggleAppearance newAppearance: Application.Appearance, 119 | application: Application) { 120 | applicationsViewController.toggle(newAppearance, for: application) 121 | } 122 | 123 | // MARK: - SystemPreferenceCollectionViewControllerDelegate 124 | 125 | func systemPreferenceViewController(_ controller: SystemPreferenceFeatureViewController, 126 | toggleSystemPreference model: SystemPreferenceViewModel) { 127 | preferencesViewController.toggle(model.preference) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/Features/SystemPreferences/SystemLogicController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | protocol SystemLogicControllerDelegate: class { 4 | func systemLogicController(_ controller: SystemLogicController, 5 | didLoadPreferences preferences: [SystemPreferenceViewModel]) 6 | } 7 | 8 | class SystemLogicController { 9 | weak var delegate: SystemLogicControllerDelegate? 10 | 11 | func readSystemPreferences() -> [SystemPreferenceViewModel] { 12 | let icon = NSImage(named: .init("System Appearance"))! 13 | let preference = SystemPreference(icon: icon, 14 | name: "System".localized, 15 | bundleIdentifier: "com.apple.dock", 16 | value: true, 17 | type: .appleScript, 18 | script: """ 19 | tell application "System Events" 20 | tell appearance preferences 21 | set dark mode to not dark mode 22 | end tell 23 | end tell 24 | """) 25 | 26 | let subtitle = NSApplication.shared.mainWindow?.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua 27 | ? "Dark appearance".localized : "Light appearance".localized 28 | 29 | let systemPreferences = [ 30 | SystemPreferenceViewModel(icon: icon, 31 | title: preference.name, 32 | subtitle: subtitle, 33 | preference: preference) 34 | ] 35 | 36 | return systemPreferences 37 | } 38 | 39 | func load() { 40 | delegate?.systemLogicController(self, didLoadPreferences: readSystemPreferences()) 41 | } 42 | 43 | func toggleSystemPreference(_ systemPreference: SystemPreference) { 44 | switch systemPreference.type { 45 | case .appleScript: 46 | var error: NSDictionary? 47 | NSAppleScript(source: systemPreference.script)?.executeAndReturnError(&error) 48 | if error != nil { 49 | requestPermission { (_) in } 50 | } else { 51 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in 52 | guard let strongSelf = self else { return } 53 | strongSelf.delegate?.systemLogicController(strongSelf, didLoadPreferences: strongSelf.readSystemPreferences()) 54 | } 55 | } 56 | case .shellScript: 57 | break 58 | } 59 | } 60 | 61 | private func showSystemPreferencesDialog(handler completion : (Bool)->Void) { 62 | let alert = NSAlert() 63 | alert.messageText = "Enable Accessibility.".localized 64 | alert.informativeText = "For this Gray to work properly, you will have to enable accessibility. You can do this by adding it under the Privacy-tab under Security & Privacy in System Preferences.".localized 65 | alert.alertStyle = .informational 66 | alert.addButton(withTitle: "System Preferences".localized) 67 | alert.addButton(withTitle: "OK".localized) 68 | completion(alert.runModal() == .alertFirstButtonReturn) 69 | } 70 | 71 | func requestPermission(retryOnInternalError: Bool = true, 72 | then process: @escaping (_ authorized: Bool) -> Void 73 | ) { 74 | DispatchQueue.global().async { 75 | let systemEvents = "com.apple.systemevents" 76 | NSWorkspace.shared.launchApplication( 77 | withBundleIdentifier: systemEvents, 78 | additionalEventParamDescriptor: nil, 79 | launchIdentifier: nil 80 | ) 81 | let target = NSAppleEventDescriptor(bundleIdentifier: systemEvents) 82 | let status = AEDeterminePermissionToAutomateTarget(target.aeDesc, typeWildCard, typeWildCard, true) 83 | switch Int(status) { 84 | case Int(noErr): 85 | return process(true) 86 | case errAEEventNotPermitted: 87 | break 88 | case errOSAInvalidID, -1751, 89 | errAEEventWouldRequireUserConsent, 90 | procNotFound: 91 | if retryOnInternalError { 92 | self.requestPermission(retryOnInternalError: false, then: process) 93 | } 94 | default: 95 | break 96 | } 97 | process(false) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/Features/SystemPreferences/SystemPreference.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | struct SystemPreference: Hashable { 4 | enum ScriptType: String { 5 | case appleScript 6 | case shellScript 7 | } 8 | 9 | let icon: NSImage 10 | let name: String 11 | let bundleIdentifier: String 12 | let value: Bool 13 | let type: ScriptType 14 | let script: String 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Features/SystemPreferences/SystemPreferenceFeatureViewController.swift: -------------------------------------------------------------------------------- 1 | import Blueprints 2 | import Cocoa 3 | import UserInterface 4 | 5 | protocol SystemPreferenceFeatureViewControllerDelegate: class { 6 | func systemPreferenceViewController(_ controller: SystemPreferenceFeatureViewController, 7 | toggleSystemPreference model: SystemPreferenceViewModel) 8 | } 9 | 10 | class SystemPreferenceFeatureViewController: NSViewController, NSCollectionViewDelegate, SystemLogicControllerDelegate { 11 | weak var delegate: SystemPreferenceFeatureViewControllerDelegate? 12 | let logicController = SystemLogicController() 13 | let iconStore: IconStore 14 | let component: SystemPreferenceViewController 15 | 16 | init(iconStore: IconStore) { 17 | let layoutFactory = LayoutFactory() 18 | self.iconStore = iconStore 19 | self.component = SystemPreferenceViewController(title: "Preferences".localized, 20 | layout: layoutFactory.createGridLayout(), 21 | iconStore: iconStore) 22 | super.init(nibName: nil, bundle: nil) 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | override func loadView() { 30 | self.view = component.view 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | logicController.delegate = self 36 | let backgroundView = NSView() 37 | backgroundView.wantsLayer = true 38 | backgroundView.layer?.backgroundColor = NSColor.quaternaryLabelColor.cgColor 39 | component.collectionView.backgroundView = backgroundView 40 | component.collectionView.delegate = self 41 | component.collectionView.isSelectable = true 42 | component.collectionView.allowsMultipleSelection = false 43 | } 44 | 45 | override func viewDidAppear() { 46 | super.viewDidAppear() 47 | logicController.load() 48 | } 49 | 50 | func toggle(_ systemPreference: SystemPreference) { 51 | logicController.toggleSystemPreference(systemPreference) 52 | } 53 | 54 | // MARK: - SystemLogicControllerDelegate 55 | 56 | func systemLogicController(_ controller: SystemLogicController, didLoadPreferences preferences: [SystemPreferenceViewModel]) { 57 | component.reload(with: preferences) 58 | } 59 | 60 | // MARK: - NSCollectionViewDelegate 61 | 62 | func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { 63 | guard let indexPath = indexPaths.first else { return } 64 | 65 | collectionView.deselectAll(nil) 66 | delegate?.systemPreferenceViewController(self, toggleSystemPreference: component.model(at: indexPath)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Features/SystemPreferences/SystemPreferenceView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import UserInterface 3 | 4 | // sourcery: let preference = SystemPreference 5 | class SystemPreferenceView: NSCollectionViewItem, CollectionViewItemComponent { 6 | weak var delegate: ApplicationGridViewDelegate? 7 | 8 | // sourcery: let icon: NSImage = "iconView.image = model.icon" 9 | lazy var iconView: NSImageView = .init() 10 | // sourcery: let title: String = "titleLabel.stringValue = model.title" 11 | lazy var titleLabel: NSTextField = .init() 12 | // sourcery: let subtitle: String = "subtitleLabel.stringValue = model.subtitle" 13 | lazy var subtitleLabel: NSTextField = .init() 14 | 15 | override func loadView() { 16 | let view = NSView() 17 | view.wantsLayer = true 18 | self.view = view 19 | } 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | view.layer?.backgroundColor = NSColor.white.cgColor 25 | view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor 26 | view.layer?.borderWidth = 1.0 27 | view.layer?.cornerRadius = 20 28 | view.layer?.masksToBounds = true 29 | 30 | titleLabel.backgroundColor = .clear 31 | titleLabel.isBezeled = false 32 | titleLabel.isEditable = false 33 | titleLabel.maximumNumberOfLines = 2 34 | titleLabel.font = NSFont.boldSystemFont(ofSize: 13) 35 | 36 | subtitleLabel.backgroundColor = .clear 37 | subtitleLabel.isBezeled = false 38 | subtitleLabel.isEditable = false 39 | subtitleLabel.maximumNumberOfLines = 1 40 | subtitleLabel.font = NSFont.boldSystemFont(ofSize: 11) 41 | 42 | view.addSubviews(iconView, titleLabel, subtitleLabel) 43 | 44 | let margin: CGFloat = 18 45 | 46 | NSLayoutConstraint.constrain( 47 | iconView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin), 48 | iconView.topAnchor.constraint(equalTo: view.topAnchor, constant: margin), 49 | iconView.widthAnchor.constraint(equalToConstant: 20), 50 | iconView.heightAnchor.constraint(equalToConstant: 20), 51 | 52 | titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin), 53 | titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin), 54 | titleLabel.bottomAnchor.constraint(equalTo: subtitleLabel.topAnchor, constant: 0), 55 | 56 | subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin), 57 | subtitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin), 58 | subtitleLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -margin) 59 | ) 60 | 61 | configureAppearance() 62 | } 63 | 64 | override func viewDidLayout() { 65 | super.viewDidLayout() 66 | configureAppearance() 67 | } 68 | 69 | private func configureAppearance() { 70 | switch view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) { 71 | case .darkAqua?: 72 | view.layer?.backgroundColor = NSColor(named: "Dark")?.cgColor 73 | titleLabel.animator().textColor = .white 74 | subtitleLabel.textColor = .lightGray 75 | case .aqua?: 76 | view.layer?.backgroundColor = .white 77 | titleLabel.textColor = .black 78 | subtitleLabel.textColor = .darkGray 79 | default: 80 | break 81 | } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /Sources/Features/Views/OpaqueView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class OpaqueView: NSView { 4 | override var isOpaque: Bool { return true } 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Shared/CollectionViewHeader.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import UserInterface 3 | 4 | class CollectionViewHeader: NSView { 5 | lazy var customTextField = NSTextField() 6 | 7 | override init(frame frameRect: NSRect) { 8 | super.init(frame: frameRect) 9 | loadView() 10 | } 11 | 12 | required init?(coder: NSCoder) { 13 | fatalError("init(coder:) has not been implemented") 14 | } 15 | 16 | @objc func loadView() { 17 | addSubview(customTextField) 18 | NSLayoutConstraint.constrain( 19 | customTextField.centerYAnchor.constraint(equalTo: centerYAnchor), 20 | customTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15), 21 | customTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15) 22 | ) 23 | customTextField.isBezeled = false 24 | customTextField.isBordered = false 25 | customTextField.isEditable = false 26 | customTextField.isSelectable = false 27 | customTextField.drawsBackground = false 28 | customTextField.font = NSFont.boldSystemFont(ofSize: 18) 29 | } 30 | 31 | func setText(_ text: String) { 32 | self.customTextField.stringValue = text 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Utilities/IconController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class IconController { 4 | let cache = NSCache() 5 | 6 | func icon(for application: Application) -> NSImage? { 7 | if let image = cache.object(forKey: application.url.path as NSString) { 8 | return image 9 | } 10 | 11 | var image: NSImage 12 | if let cachedImage = loadImageFromDisk(for: application) { 13 | image = cachedImage 14 | return image 15 | } else { 16 | image = NSWorkspace.shared.icon(forFile: application.url.path) 17 | var imageRect: CGRect = .init(origin: .zero, size: CGSize(width: 32, height: 32)) 18 | let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) 19 | if let imageRef = imageRef { 20 | image = NSImage(cgImage: imageRef, size: imageRect.size) 21 | } 22 | } 23 | 24 | saveImageToDisk(image, application: application) 25 | cache.setObject(image, forKey: application.url.path as NSString) 26 | return image 27 | } 28 | 29 | func loadImageFromDisk(for application: Application) -> NSImage? { 30 | if let applicationFile = try? applicationCacheDirectory() 31 | .appendingPathComponent("\(application.bundleIdentifier).tiff") { 32 | if FileManager.default.fileExists(atPath: applicationFile.path) { 33 | let image = NSImage.init(contentsOf: applicationFile) 34 | return image 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func saveImageToDisk(_ image: NSImage, application: Application) { 42 | do { 43 | let applicationFile = try applicationCacheDirectory() 44 | .appendingPathComponent("\(application.bundleIdentifier).tiff") 45 | 46 | if let tiff = image.tiffRepresentation { 47 | if let imgRep = NSBitmapImageRep(data: tiff) { 48 | if let data = imgRep.representation(using: .tiff, properties: [:]) { 49 | try data.write(to: applicationFile) 50 | } 51 | } 52 | } 53 | 54 | } catch let error { 55 | Swift.print(error) 56 | } 57 | } 58 | 59 | func applicationCacheDirectory() throws -> URL { 60 | let url = try FileManager.default.url(for: .cachesDirectory, 61 | in: .userDomainMask, 62 | appropriateFor: nil, 63 | create: true) 64 | .appendingPathComponent(Bundle.main.bundleIdentifier!) 65 | .appendingPathComponent("IconCache") 66 | 67 | if !FileManager.default.fileExists(atPath: url.path) { 68 | try FileManager.default.createDirectory(at: url, 69 | withIntermediateDirectories: false, 70 | attributes: nil) 71 | } 72 | 73 | return url 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Utilities/Shell.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Shell { 4 | @discardableResult func execute(command: String, 5 | arguments: [String] = [], 6 | at path: String = ".") -> String { 7 | let process = Process() 8 | let path = path.replacingOccurrences(of: " ", with: "\\ ") 9 | let arguments = arguments.joined(separator: " ") 10 | let command = "cd \(path) && \(command) \(arguments)" 11 | return process.shell(command: command) 12 | } 13 | } 14 | 15 | extension Process { 16 | public func shell(command: String) -> String { 17 | let outputQueue = DispatchQueue.init(label: "shell-queue") 18 | let outputPipe = Pipe() 19 | let errorPipe = Pipe() 20 | 21 | launchPath = "/bin/bash" 22 | arguments = ["-c", command] 23 | standardOutput = outputPipe 24 | standardError = errorPipe 25 | 26 | var result = Data() 27 | var error = Data() 28 | 29 | outputPipe.fileHandleForReading.readabilityHandler = { handler in 30 | outputQueue.async { result.append(handler.availableData) } 31 | } 32 | 33 | errorPipe.fileHandleForReading.readabilityHandler = { handler in 34 | outputQueue.async { error.append(handler.availableData) } 35 | } 36 | 37 | launch() 38 | waitUntilExit() 39 | 40 | outputPipe.fileHandleForReading.readabilityHandler = nil 41 | errorPipe.fileHandleForReading.readabilityHandler = nil 42 | 43 | return outputQueue.sync { 44 | return result.string() 45 | } 46 | } 47 | } 48 | 49 | fileprivate extension Data { 50 | func string() -> String { 51 | guard let output = String(data: self, encoding: .utf8) else { return "" } 52 | 53 | guard !output.hasSuffix("\n") else { 54 | let endIndex = output.index(before: output.endIndex) 55 | return String(output[.. String { 12 | return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String 13 | } 14 | 15 | func checkForNewVersion(then handler: @escaping (Bool) -> Void) { 16 | let url = URL(string: "https://raw.githubusercontent.com/zenangst/Gray/master/.current-version")! 17 | let task = URLSession.shared.dataTask(with: url) { 18 | [weak self] (data, response, error) in 19 | guard let strongSelf = self, 20 | let data = data, 21 | let body = String.init(data: data, encoding: .utf8) else { 22 | DispatchQueue.main.async { handler(false) } 23 | return 24 | } 25 | 26 | let newVersion = body.replacingOccurrences(of: "\n", with: "") 27 | 28 | if strongSelf.currentVersion() != newVersion, 29 | newVersion.compare(strongSelf.currentVersion(), options: .numeric) == .orderedDescending { 30 | DispatchQueue.main.async { 31 | strongSelf.delegate?.versionController(strongSelf, foundNewVersion: newVersion) 32 | } 33 | } else { 34 | DispatchQueue.main.async { handler(false) } 35 | } 36 | } 37 | task.resume() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Voodoo/Output/CollectionViewItemComponent-macOS.generated.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 0.17.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import Cocoa 5 | import Differific 6 | 7 | class ApplicationGridViewController: NSViewController, Component { 8 | private let layout: NSCollectionViewFlowLayout 9 | private let dataSource: ApplicationGridDataSource 10 | let collectionView: NSCollectionView 11 | 12 | init(title: String? = nil, 13 | layout: NSCollectionViewFlowLayout, 14 | iconStore: IconStore, 15 | collectionView: NSCollectionView? = nil) { 16 | self.layout = layout 17 | self.dataSource = ApplicationGridDataSource(title: title, iconStore: iconStore) 18 | if let collectionView = collectionView { 19 | self.collectionView = collectionView 20 | } else { 21 | self.collectionView = NSCollectionView() 22 | } 23 | self.collectionView.collectionViewLayout = layout 24 | super.init(nibName: nil, bundle: nil) 25 | self.title = title 26 | } 27 | 28 | required init?(coder aDecoder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | // MARK: - View lifecycle 33 | 34 | override func loadView() { 35 | self.view = collectionView 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | collectionView.dataSource = dataSource 41 | let headerIdentifier = NSUserInterfaceItemIdentifier.init("ApplicationGridViewHeader") 42 | collectionView.register(CollectionViewHeader.self, 43 | forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, 44 | withIdentifier: headerIdentifier) 45 | let itemIdentifier = NSUserInterfaceItemIdentifier.init("ApplicationGridView") 46 | collectionView.register(ApplicationGridView.self, forItemWithIdentifier: itemIdentifier) 47 | 48 | if title != nil { 49 | layout.headerReferenceSize.height = 60 50 | } 51 | } 52 | 53 | // MARK: - Public API 54 | 55 | func indexPath(for item: NSCollectionViewItem) -> IndexPath? { 56 | return collectionView.indexPath(for: item) 57 | } 58 | 59 | func model(at indexPath: IndexPath) -> ApplicationGridViewModel { 60 | return dataSource.model(at: indexPath) 61 | } 62 | 63 | func reload(with models: [ApplicationGridViewModel], completion: (() -> Void)? = nil) { 64 | dataSource.reload(collectionView, with: models, then: completion) 65 | } 66 | } 67 | 68 | class ApplicationGridDataSource: NSObject, NSCollectionViewDataSource { 69 | 70 | private var title: String? 71 | private var models = [ApplicationGridViewModel]() 72 | private let iconStore: IconStore 73 | 74 | init(title: String? = nil, 75 | models: [ApplicationGridViewModel] = [], 76 | iconStore: IconStore) { 77 | self.title = title 78 | self.models = models 79 | self.iconStore = iconStore 80 | super.init() 81 | } 82 | 83 | // MARK: - Public API 84 | 85 | func model(at indexPath: IndexPath) -> ApplicationGridViewModel { 86 | return models[indexPath.item] 87 | } 88 | 89 | func reload(_ collectionView: NSCollectionView, 90 | with models: [ApplicationGridViewModel], 91 | then handler: (() -> Void)? = nil) { 92 | let manager = DiffManager() 93 | let changes = manager.diff(self.models, models) 94 | collectionView.reload(with: changes, 95 | updateDataSource: { self.models = models }, 96 | completion: handler) 97 | } 98 | 99 | // MARK: - NSCollectionViewDataSource 100 | 101 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 102 | return models.count 103 | } 104 | 105 | func collectionView(_ collectionView: NSCollectionView, 106 | viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, 107 | at indexPath: IndexPath) -> NSView { 108 | let identifier = NSUserInterfaceItemIdentifier.init("ApplicationGridViewHeader") 109 | let item = collectionView.makeSupplementaryView(ofKind: NSCollectionView.elementKindSectionHeader, 110 | withIdentifier: identifier, for: indexPath) 111 | 112 | if let title = title, let header = item as? CollectionViewHeader { 113 | header.setText(title) 114 | } 115 | 116 | return item 117 | } 118 | 119 | func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 120 | let identifier = NSUserInterfaceItemIdentifier.init("ApplicationGridView") 121 | let item = collectionView.makeItem(withIdentifier: identifier, for: indexPath) 122 | let model = self.model(at: indexPath) 123 | 124 | if let view = item as? ApplicationGridView { 125 | view.currentAppearance = model.application.appearance 126 | iconStore.loadIcon(for: model.application) { image in view.iconView.image = image } 127 | view.titleLabel.stringValue = model.application.localizedName ?? model.title 128 | view.subtitleLabel.stringValue = model.subtitle 129 | } 130 | 131 | return item 132 | } 133 | } 134 | 135 | struct ApplicationGridViewModel: Hashable { 136 | let title: String 137 | let subtitle: String 138 | let application: Application 139 | } 140 | 141 | class ApplicationListViewController: NSViewController, Component { 142 | private let layout: NSCollectionViewFlowLayout 143 | private let dataSource: ApplicationListDataSource 144 | let collectionView: NSCollectionView 145 | 146 | init(title: String? = nil, 147 | layout: NSCollectionViewFlowLayout, 148 | iconStore: IconStore, 149 | collectionView: NSCollectionView? = nil) { 150 | self.layout = layout 151 | self.dataSource = ApplicationListDataSource(title: title, iconStore: iconStore) 152 | if let collectionView = collectionView { 153 | self.collectionView = collectionView 154 | } else { 155 | self.collectionView = NSCollectionView() 156 | } 157 | self.collectionView.collectionViewLayout = layout 158 | super.init(nibName: nil, bundle: nil) 159 | self.title = title 160 | } 161 | 162 | required init?(coder aDecoder: NSCoder) { 163 | fatalError("init(coder:) has not been implemented") 164 | } 165 | 166 | // MARK: - View lifecycle 167 | 168 | override func loadView() { 169 | self.view = collectionView 170 | } 171 | 172 | override func viewDidLoad() { 173 | super.viewDidLoad() 174 | collectionView.dataSource = dataSource 175 | let headerIdentifier = NSUserInterfaceItemIdentifier.init("ApplicationListViewHeader") 176 | collectionView.register(CollectionViewHeader.self, 177 | forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, 178 | withIdentifier: headerIdentifier) 179 | let itemIdentifier = NSUserInterfaceItemIdentifier.init("ApplicationListView") 180 | collectionView.register(ApplicationListView.self, forItemWithIdentifier: itemIdentifier) 181 | 182 | if title != nil { 183 | layout.headerReferenceSize.height = 60 184 | } 185 | } 186 | 187 | // MARK: - Public API 188 | 189 | func indexPath(for item: NSCollectionViewItem) -> IndexPath? { 190 | return collectionView.indexPath(for: item) 191 | } 192 | 193 | func model(at indexPath: IndexPath) -> ApplicationListViewModel { 194 | return dataSource.model(at: indexPath) 195 | } 196 | 197 | func reload(with models: [ApplicationListViewModel], completion: (() -> Void)? = nil) { 198 | dataSource.reload(collectionView, with: models, then: completion) 199 | } 200 | } 201 | 202 | class ApplicationListDataSource: NSObject, NSCollectionViewDataSource { 203 | 204 | private var title: String? 205 | private var models = [ApplicationListViewModel]() 206 | private let iconStore: IconStore 207 | 208 | init(title: String? = nil, 209 | models: [ApplicationListViewModel] = [], 210 | iconStore: IconStore) { 211 | self.title = title 212 | self.models = models 213 | self.iconStore = iconStore 214 | super.init() 215 | } 216 | 217 | // MARK: - Public API 218 | 219 | func model(at indexPath: IndexPath) -> ApplicationListViewModel { 220 | return models[indexPath.item] 221 | } 222 | 223 | func reload(_ collectionView: NSCollectionView, 224 | with models: [ApplicationListViewModel], 225 | then handler: (() -> Void)? = nil) { 226 | let manager = DiffManager() 227 | let changes = manager.diff(self.models, models) 228 | collectionView.reload(with: changes, 229 | updateDataSource: { self.models = models }, 230 | completion: handler) 231 | } 232 | 233 | // MARK: - NSCollectionViewDataSource 234 | 235 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 236 | return models.count 237 | } 238 | 239 | func collectionView(_ collectionView: NSCollectionView, 240 | viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, 241 | at indexPath: IndexPath) -> NSView { 242 | let identifier = NSUserInterfaceItemIdentifier.init("ApplicationListViewHeader") 243 | let item = collectionView.makeSupplementaryView(ofKind: NSCollectionView.elementKindSectionHeader, 244 | withIdentifier: identifier, for: indexPath) 245 | 246 | if let title = title, let header = item as? CollectionViewHeader { 247 | header.setText(title) 248 | } 249 | 250 | return item 251 | } 252 | 253 | func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 254 | let identifier = NSUserInterfaceItemIdentifier.init("ApplicationListView") 255 | let item = collectionView.makeItem(withIdentifier: identifier, for: indexPath) 256 | let model = self.model(at: indexPath) 257 | 258 | if let view = item as? ApplicationListView { 259 | view.currentAppearance = model.application.appearance 260 | iconStore.loadIcon(for: model.application) { image in view.iconView.image = image } 261 | view.titleLabel.stringValue = model.application.localizedName ?? model.title 262 | view.subtitleLabel.stringValue = model.subtitle 263 | } 264 | 265 | return item 266 | } 267 | } 268 | 269 | struct ApplicationListViewModel: Hashable { 270 | let title: String 271 | let subtitle: String 272 | let application: Application 273 | } 274 | 275 | class SystemPreferenceViewController: NSViewController, Component { 276 | private let layout: NSCollectionViewFlowLayout 277 | private let dataSource: SystemPreferenceDataSource 278 | let collectionView: NSCollectionView 279 | 280 | init(title: String? = nil, 281 | layout: NSCollectionViewFlowLayout, 282 | iconStore: IconStore, 283 | collectionView: NSCollectionView? = nil) { 284 | self.layout = layout 285 | self.dataSource = SystemPreferenceDataSource(title: title, iconStore: iconStore) 286 | if let collectionView = collectionView { 287 | self.collectionView = collectionView 288 | } else { 289 | self.collectionView = NSCollectionView() 290 | } 291 | self.collectionView.collectionViewLayout = layout 292 | super.init(nibName: nil, bundle: nil) 293 | self.title = title 294 | } 295 | 296 | required init?(coder aDecoder: NSCoder) { 297 | fatalError("init(coder:) has not been implemented") 298 | } 299 | 300 | // MARK: - View lifecycle 301 | 302 | override func loadView() { 303 | self.view = collectionView 304 | } 305 | 306 | override func viewDidLoad() { 307 | super.viewDidLoad() 308 | collectionView.dataSource = dataSource 309 | let headerIdentifier = NSUserInterfaceItemIdentifier.init("SystemPreferenceViewHeader") 310 | collectionView.register(CollectionViewHeader.self, 311 | forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, 312 | withIdentifier: headerIdentifier) 313 | let itemIdentifier = NSUserInterfaceItemIdentifier.init("SystemPreferenceView") 314 | collectionView.register(SystemPreferenceView.self, forItemWithIdentifier: itemIdentifier) 315 | 316 | if title != nil { 317 | layout.headerReferenceSize.height = 60 318 | } 319 | } 320 | 321 | // MARK: - Public API 322 | 323 | func indexPath(for item: NSCollectionViewItem) -> IndexPath? { 324 | return collectionView.indexPath(for: item) 325 | } 326 | 327 | func model(at indexPath: IndexPath) -> SystemPreferenceViewModel { 328 | return dataSource.model(at: indexPath) 329 | } 330 | 331 | func reload(with models: [SystemPreferenceViewModel], completion: (() -> Void)? = nil) { 332 | dataSource.reload(collectionView, with: models, then: completion) 333 | } 334 | } 335 | 336 | class SystemPreferenceDataSource: NSObject, NSCollectionViewDataSource { 337 | 338 | private var title: String? 339 | private var models = [SystemPreferenceViewModel]() 340 | private let iconStore: IconStore 341 | 342 | init(title: String? = nil, 343 | models: [SystemPreferenceViewModel] = [], 344 | iconStore: IconStore) { 345 | self.title = title 346 | self.models = models 347 | self.iconStore = iconStore 348 | super.init() 349 | } 350 | 351 | // MARK: - Public API 352 | 353 | func model(at indexPath: IndexPath) -> SystemPreferenceViewModel { 354 | return models[indexPath.item] 355 | } 356 | 357 | func reload(_ collectionView: NSCollectionView, 358 | with models: [SystemPreferenceViewModel], 359 | then handler: (() -> Void)? = nil) { 360 | let manager = DiffManager() 361 | let changes = manager.diff(self.models, models) 362 | collectionView.reload(with: changes, 363 | updateDataSource: { self.models = models }, 364 | completion: handler) 365 | } 366 | 367 | // MARK: - NSCollectionViewDataSource 368 | 369 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 370 | return models.count 371 | } 372 | 373 | func collectionView(_ collectionView: NSCollectionView, 374 | viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, 375 | at indexPath: IndexPath) -> NSView { 376 | let identifier = NSUserInterfaceItemIdentifier.init("SystemPreferenceViewHeader") 377 | let item = collectionView.makeSupplementaryView(ofKind: NSCollectionView.elementKindSectionHeader, 378 | withIdentifier: identifier, for: indexPath) 379 | 380 | if let title = title, let header = item as? CollectionViewHeader { 381 | header.setText(title) 382 | } 383 | 384 | return item 385 | } 386 | 387 | func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 388 | let identifier = NSUserInterfaceItemIdentifier.init("SystemPreferenceView") 389 | let item = collectionView.makeItem(withIdentifier: identifier, for: indexPath) 390 | let model = self.model(at: indexPath) 391 | 392 | if let view = item as? SystemPreferenceView { 393 | view.iconView.image = model.icon 394 | view.titleLabel.stringValue = model.title 395 | view.subtitleLabel.stringValue = model.subtitle 396 | } 397 | 398 | return item 399 | } 400 | } 401 | 402 | struct SystemPreferenceViewModel: Hashable { 403 | let icon: NSImage 404 | let title: String 405 | let subtitle: String 406 | let preference: SystemPreference 407 | } 408 | 409 | -------------------------------------------------------------------------------- /Voodoo/Output/StatefulItem-macOS.generated.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 0.17.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import Cocoa 5 | 6 | -------------------------------------------------------------------------------- /Voodoo/Output/ViewControllerFactory-macOS.generated.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 0.17.0 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | import Cocoa 5 | 6 | class ViewControllerFactory { 7 | 8 | public func createApplicationGridViewController(layout: NSCollectionViewFlowLayout, iconStore: IconStore) -> ApplicationGridViewController { 9 | let viewController = ApplicationGridViewController(layout: layout, iconStore: iconStore) 10 | return viewController 11 | } 12 | public func createApplicationListViewController(layout: NSCollectionViewFlowLayout, iconStore: IconStore) -> ApplicationListViewController { 13 | let viewController = ApplicationListViewController(layout: layout, iconStore: iconStore) 14 | return viewController 15 | } 16 | public func createSystemPreferenceViewController(layout: NSCollectionViewFlowLayout, iconStore: IconStore) -> SystemPreferenceViewController { 17 | let viewController = SystemPreferenceViewController(layout: layout, iconStore: iconStore) 18 | return viewController 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Voodoo/Protocols/CollectionViewItemComponent.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | protocol CollectionViewItemComponent where Self : NSCollectionViewItem {} 4 | -------------------------------------------------------------------------------- /Voodoo/Protocols/StatefulItem.swift: -------------------------------------------------------------------------------- 1 | protocol StatefulItem {} 2 | -------------------------------------------------------------------------------- /Voodoo/Templates/CollectionViewItemComponent-macOS.stencil: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Differific 3 | 4 | {% for type in types.implementing.CollectionViewItemComponent %} 5 | class {{type.name|replace:"View",""}}ViewController: NSViewController, Component { 6 | private let layout: NSCollectionViewFlowLayout 7 | private let dataSource: {{type.name|replace:"View",""}}DataSource 8 | let collectionView: NSCollectionView 9 | 10 | init(title: String? = nil, 11 | layout: NSCollectionViewFlowLayout, 12 | iconStore: IconStore, 13 | collectionView: NSCollectionView? = nil) { 14 | self.layout = layout 15 | self.dataSource = {{type.name|replace:"View",""}}DataSource(title: title, iconStore: iconStore) 16 | if let collectionView = collectionView { 17 | self.collectionView = collectionView 18 | } else { 19 | self.collectionView = NSCollectionView() 20 | } 21 | self.collectionView.collectionViewLayout = layout 22 | super.init(nibName: nil, bundle: nil) 23 | self.title = title 24 | } 25 | 26 | required init?(coder aDecoder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | // MARK: - View lifecycle 31 | 32 | override func loadView() { 33 | self.view = collectionView 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | collectionView.dataSource = dataSource 39 | let headerIdentifier = NSUserInterfaceItemIdentifier.init("{{type.name}}Header") 40 | collectionView.register(CollectionViewHeader.self, 41 | forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, 42 | withIdentifier: headerIdentifier) 43 | let itemIdentifier = NSUserInterfaceItemIdentifier.init("{{type.name}}") 44 | collectionView.register({{type.name}}.self, forItemWithIdentifier: itemIdentifier) 45 | 46 | if title != nil { 47 | layout.headerReferenceSize.height = 60 48 | } 49 | } 50 | 51 | // MARK: - Public API 52 | 53 | func indexPath(for item: NSCollectionViewItem) -> IndexPath? { 54 | return collectionView.indexPath(for: item) 55 | } 56 | 57 | func model(at indexPath: IndexPath) -> {{type.name}}Model { 58 | return dataSource.model(at: indexPath) 59 | } 60 | 61 | func reload(with models: [{{type.name}}Model], completion: (() -> Void)? = nil) { 62 | dataSource.reload(collectionView, with: models, then: completion) 63 | } 64 | } 65 | 66 | class {{type.name|replace:"View",""}}DataSource: NSObject, NSCollectionViewDataSource { 67 | 68 | private var title: String? 69 | private var models = [{{type.name}}Model]() 70 | private let iconStore: IconStore 71 | 72 | init(title: String? = nil, 73 | models: [{{type.name}}Model] = [], 74 | iconStore: IconStore) { 75 | self.title = title 76 | self.models = models 77 | self.iconStore = iconStore 78 | super.init() 79 | } 80 | 81 | // MARK: - Public API 82 | 83 | func model(at indexPath: IndexPath) -> {{type.name}}Model { 84 | return models[indexPath.item] 85 | } 86 | 87 | func reload(_ collectionView: NSCollectionView, 88 | with models: [{{type.name}}Model], 89 | then handler: (() -> Void)? = nil) { 90 | let manager = DiffManager() 91 | let changes = manager.diff(self.models, models) 92 | collectionView.reload(with: changes, 93 | updateDataSource: { self.models = models }, 94 | completion: handler) 95 | } 96 | 97 | // MARK: - NSCollectionViewDataSource 98 | 99 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 100 | return models.count 101 | } 102 | 103 | func collectionView(_ collectionView: NSCollectionView, 104 | viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, 105 | at indexPath: IndexPath) -> NSView { 106 | let identifier = NSUserInterfaceItemIdentifier.init("{{type.name}}Header") 107 | let item = collectionView.makeSupplementaryView(ofKind: NSCollectionView.elementKindSectionHeader, 108 | withIdentifier: identifier, for: indexPath) 109 | 110 | if let title = title, let header = item as? CollectionViewHeader { 111 | header.setText(title) 112 | } 113 | 114 | return item 115 | } 116 | 117 | func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 118 | let identifier = NSUserInterfaceItemIdentifier.init("{{type.name}}") 119 | let item = collectionView.makeItem(withIdentifier: identifier, for: indexPath) 120 | let model = self.model(at: indexPath) 121 | 122 | if let view = item as? {{type.name}} { 123 | {% for variable in type.variables %} 124 | {% for key, value in variable.annotations %} 125 | {% ifnot variable|annotated:"$RawBinding" %} 126 | {% if value.count > 1 %} 127 | view.{{key}} = {{value}} 128 | {% else %} 129 | {% for variableType in value %} 130 | view.{{variable.annotations[key][variableType]}} 131 | {% endfor %} 132 | {% endif %} 133 | {% else %} 134 | {{ value }} 135 | {% endif %} 136 | {% endfor %} 137 | {% endfor %} 138 | } 139 | 140 | return item 141 | } 142 | } 143 | 144 | struct {{type.name}}Model: Hashable { 145 | {% for variable in type.variables %} 146 | {% ifnot variable|annotated:"$RawBinding" %} 147 | {% for key, value in variable.annotations %} 148 | {% for variableType in variable.annotations[key] %} 149 | {{key}}: {{variableType}} 150 | {% endfor %} 151 | {% endfor %} 152 | {% endif %} 153 | {% endfor %} 154 | {% for key in type.annotations %} 155 | {{key}}: {{type.annotations[key]}} 156 | {% endfor %} 157 | } 158 | 159 | {% endfor %} 160 | -------------------------------------------------------------------------------- /Voodoo/Templates/StatefulItem-macOS.stencil: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | {% for type in types.implementing.StatefulItem %} 4 | protocol {{type.name|replace:"View",""}}ErrorViewController : class { 5 | var error: Error { set get } 6 | } 7 | 8 | enum {{type.name}}State { 9 | case initial 10 | case loading 11 | case failure(error: Error) 12 | case success(models: [{{type.name}}Model]) 13 | } 14 | 15 | class {{type.name}}StateController: NSViewController { 16 | typealias ErrorViewControllerType = {{type.name|replace:"View",""}}ErrorViewController & NSViewController 17 | 18 | private let initialViewController: NSViewController 19 | private let loadingViewController: NSViewController 20 | private let failureViewController: ErrorViewControllerType 21 | private let successController: {{type.name|replace:"Cell",""}}ViewController 22 | 23 | public init(initialViewController: NSViewController, 24 | loadingViewController: NSViewController, 25 | failureViewController: ErrorViewControllerType, 26 | successController: {{type.name|replace:"Cell",""}}ViewController) { 27 | self.initialViewController = initialViewController 28 | self.loadingViewController = loadingViewController 29 | self.failureViewController = failureViewController 30 | self.successController = successController 31 | super.init(nibName: nil, bundle: nil) 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | private func render(_ state: {{type.name}}State) { 39 | children.forEach { 40 | $0.removeFromParent() 41 | } 42 | let viewController: NSViewController 43 | switch state { 44 | case .initial: 45 | viewController = initialViewController 46 | case .loading: 47 | viewController = loadingViewController 48 | case .failure(let error): 49 | viewController = failureViewController 50 | failureViewController.error = error 51 | case .success(let models): 52 | viewController = successController 53 | successController.reload(with: models) 54 | } 55 | addChild(viewController) 56 | viewController.view.frame = view.bounds 57 | view.addSubview(viewController.view) 58 | } 59 | } 60 | 61 | {% endfor %} 62 | -------------------------------------------------------------------------------- /Voodoo/Templates/ViewControllerFactory-macOS.stencil: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class ViewControllerFactory { 4 | {% for type in types.implementing.StatefulItem %} 5 | public func create{{type.name}}StateController(layout: NSCollectionViewFlowLayout, 6 | initialViewController: NSViewController = .init(), 7 | loadingViewController: NSViewController = .init(), 8 | failureViewController: {{type.name}}StateController.ErrorViewControllerType) -> {{type.name}}StateController { 9 | let viewController = create{{type.name|replace:"View",""}}ViewController(layout: layout) 10 | let stateController = {{type.name}}StateController( 11 | initialViewController: initialViewController, 12 | loadingViewController: loadingViewController, 13 | failureViewController: failureViewController, 14 | successController: viewController 15 | ) 16 | 17 | return stateController 18 | } 19 | {% endfor %} 20 | 21 | {% for type in types.implementing.CollectionViewItemComponent %} 22 | public func create{{type.name|replace:"View",""}}ViewController(layout: NSCollectionViewFlowLayout, iconStore: IconStore) -> {{type.name|replace:"View",""}}ViewController { 23 | let viewController = {{type.name|replace:"View",""}}ViewController(layout: layout, iconStore: iconStore) 24 | return viewController 25 | } 26 | {% endfor %} 27 | } 28 | 29 | --------------------------------------------------------------------------------