├── .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 | [](https://travis-ci.com/zenangst/Gray)
6 | 
7 | [](https://www.apple.com/macos/mojave/)
8 | [](https://opensource.org/licenses/MIT)
9 |
10 |
11 |
12 |
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 |
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 |
180 |
181 |
182 |
185 |
188 |
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 |
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 |
--------------------------------------------------------------------------------