├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── LICENSE.md ├── Makefile ├── README.md ├── Sukhasana.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Sukhasana.xcscheme ├── Sukhasana ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-128.png │ │ ├── icon-128@2x.png │ │ ├── icon-16.png │ │ ├── icon-16@2x.png │ │ ├── icon-256.png │ │ ├── icon-256@2x.png │ │ ├── icon-32.png │ │ ├── icon-32@2x.png │ │ ├── icon-512.png │ │ └── icon-512@2x.png │ ├── Gear.imageset │ │ ├── Contents.json │ │ └── Gear@2x.png │ ├── StatusItemIcon.imageset │ │ ├── Contents.json │ │ ├── StatusItemIcon.png │ │ └── StatusItemIcon@2x.png │ └── StatusItemIconHighlighted.imageset │ │ ├── Contents.json │ │ ├── menu-white.png │ │ └── menu-white@2x.png ├── Info.plist ├── Model │ ├── Results.swift │ └── Settings.swift ├── Other │ ├── APIClient.swift │ ├── NSUserDefaults+SettingsStore.swift │ └── SignalOperators.swift ├── View │ ├── AppDelegate.swift │ ├── MainMenu.xib │ ├── MainViewController.swift │ ├── MainViewController.xib │ ├── Panel.swift │ ├── ResultsTableView.swift │ ├── SeparatorView.swift │ ├── SettingsViewController.swift │ ├── SettingsViewController.xib │ └── ViewController.swift └── ViewModel │ ├── ApplicationModel.swift │ ├── MainScreenModel.swift │ ├── ResultsTableViewModel.swift │ └── SettingsScreenModel.swift └── SukhasanaTests ├── Info.plist └── SukhasanaTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # Carthage 21 | # 22 | Carthage 23 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "brow/Alamofire" == 1.1.4 2 | github "brow/MASShortcut" "carthage" 3 | github "brow/ReactiveCocoa" "7651d7de1c939b2a4db7ffb3220ca6274c20a9aa" 4 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "brow/Alamofire" "1.1.4" 2 | github "LlamaKit/LlamaKit" "v0.5.0" 3 | github "brow/MASShortcut" "4d3c2cad2c35e466fb28c9718e2d926b951b3066" 4 | github "brow/ReactiveCocoa" "7651d7de1c939b2a4db7ffb3220ca6274c20a9aa" 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2015, Tom Brow** 2 | **All rights reserved.** 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP=build/Release/Sukhasana.app 2 | 3 | $(APP): 4 | carthage bootstrap --platform Mac 5 | xcodebuild -configuration Release 6 | 7 | install: $(APP) 8 | cp -r $(APP) /Applications 9 | 10 | clean: 11 | rm -rf build 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sukhasana 2 | Mac status item for searching Asana. An experiment with [MVVM](https://github.com/ReactiveCocoa/ReactiveViewModel) in [ReactiveCocoa 3](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/1382). 3 | 4 | ![screenshot](http://zippy.gfycat.com/WellmadeBewitchedHart.gif) 5 | 6 | Requires OS X Yosemite (10.10). 7 | 8 | ## Installation 9 | 10 | ### Binary 11 | 12 | 1. Download `Sukhasana.zip` from the [latest release](https://github.com/brow/Sukhasana/releases/latest). 13 | 2. Open `Sukhasana.zip`. 14 | 3. Move `Sukhasana` into your `Applications` folder. 15 | 16 | ### From source 17 | Sukhasana's dependencies are built with [Carthage](https://github.com/Carthage/Carthage). You can install Carthage using [Homebrew](http://brew.sh/). 18 | ``` 19 | brew install carthage 20 | ``` 21 | Build and install with `make`. 22 | ``` 23 | git clone git@github.com:brow/Sukhasana.git 24 | cd Sukhasana 25 | make install 26 | ``` 27 | Be sure to `make clean` when you update. 28 | ``` 29 | git pull 30 | make clean install 31 | ``` 32 | 33 | ## Development 34 | ``` 35 | brew install carthage 36 | git clone git@github.com:brow/Sukhasana.git 37 | cd Sukhasana 38 | carthage bootstrap --platform Mac 39 | open Sukhasana.xcodeproj 40 | ``` 41 | -------------------------------------------------------------------------------- /Sukhasana.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | EA037E691A834B9300F47B17 /* SukhasanaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA037E681A834B9300F47B17 /* SukhasanaTests.swift */; }; 11 | EA037E7F1A847CF800F47B17 /* LlamaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA037E791A847CF800F47B17 /* LlamaKit.framework */; }; 12 | EA037E801A847CF800F47B17 /* LlamaKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA037E791A847CF800F47B17 /* LlamaKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 13 | EA037E851A847CF800F47B17 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA037E7C1A847CF800F47B17 /* ReactiveCocoa.framework */; }; 14 | EA037E861A847CF800F47B17 /* ReactiveCocoa.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA037E7C1A847CF800F47B17 /* ReactiveCocoa.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | EA125B1A1AA40426004177C4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EA125B031AA40426004177C4 /* Images.xcassets */; }; 16 | EA125B1C1AA40426004177C4 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B061AA40426004177C4 /* Settings.swift */; }; 17 | EA125B1D1AA40426004177C4 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B081AA40426004177C4 /* APIClient.swift */; }; 18 | EA125B1E1AA40426004177C4 /* NSUserDefaults+SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B091AA40426004177C4 /* NSUserDefaults+SettingsStore.swift */; }; 19 | EA125B1F1AA40426004177C4 /* SignalOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B0A1AA40426004177C4 /* SignalOperators.swift */; }; 20 | EA125B201AA40426004177C4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B0C1AA40426004177C4 /* AppDelegate.swift */; }; 21 | EA125B211AA40426004177C4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = EA125B0D1AA40426004177C4 /* MainMenu.xib */; }; 22 | EA125B221AA40426004177C4 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B0E1AA40426004177C4 /* MainViewController.swift */; }; 23 | EA125B231AA40426004177C4 /* MainViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = EA125B0F1AA40426004177C4 /* MainViewController.xib */; }; 24 | EA125B241AA40426004177C4 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B101AA40426004177C4 /* Panel.swift */; }; 25 | EA125B251AA40426004177C4 /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B111AA40426004177C4 /* SeparatorView.swift */; }; 26 | EA125B261AA40426004177C4 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B121AA40426004177C4 /* SettingsViewController.swift */; }; 27 | EA125B271AA40426004177C4 /* SettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = EA125B131AA40426004177C4 /* SettingsViewController.xib */; }; 28 | EA125B281AA40426004177C4 /* ResultsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B141AA40426004177C4 /* ResultsTableView.swift */; }; 29 | EA125B291AA40426004177C4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B151AA40426004177C4 /* ViewController.swift */; }; 30 | EA125B2A1AA40426004177C4 /* ApplicationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B171AA40426004177C4 /* ApplicationModel.swift */; }; 31 | EA125B2B1AA40426004177C4 /* MainScreenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B181AA40426004177C4 /* MainScreenModel.swift */; }; 32 | EA125B2C1AA40426004177C4 /* SettingsScreenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B191AA40426004177C4 /* SettingsScreenModel.swift */; }; 33 | EA307A601A87FC780008A566 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA307A5F1A87FC780008A566 /* Alamofire.framework */; }; 34 | EA307A611A87FC780008A566 /* Alamofire.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA307A5F1A87FC780008A566 /* Alamofire.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 35 | EAFD89FB1ABF7A0400DA0CBE /* ResultsTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD89FA1ABF7A0400DA0CBE /* ResultsTableViewModel.swift */; }; 36 | EAFD89FD1ABF7E3100DA0CBE /* Results.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD89FC1ABF7E3100DA0CBE /* Results.swift */; }; 37 | EAFFDC7D1AACF5C400F38834 /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAFFDC7C1AACF5C400F38834 /* MASShortcut.framework */; }; 38 | EAFFDC7E1AACF5C400F38834 /* MASShortcut.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EAFFDC7C1AACF5C400F38834 /* MASShortcut.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 39 | /* End PBXBuildFile section */ 40 | 41 | /* Begin PBXContainerItemProxy section */ 42 | EA037E631A834B9300F47B17 /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = EA037E4A1A834B9300F47B17 /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = EA037E511A834B9300F47B17; 47 | remoteInfo = Sukhasana; 48 | }; 49 | /* End PBXContainerItemProxy section */ 50 | 51 | /* Begin PBXCopyFilesBuildPhase section */ 52 | EA037E871A847CF800F47B17 /* Embed Frameworks */ = { 53 | isa = PBXCopyFilesBuildPhase; 54 | buildActionMask = 2147483647; 55 | dstPath = ""; 56 | dstSubfolderSpec = 10; 57 | files = ( 58 | EA037E801A847CF800F47B17 /* LlamaKit.framework in Embed Frameworks */, 59 | EA037E861A847CF800F47B17 /* ReactiveCocoa.framework in Embed Frameworks */, 60 | EAFFDC7E1AACF5C400F38834 /* MASShortcut.framework in Embed Frameworks */, 61 | EA307A611A87FC780008A566 /* Alamofire.framework in Embed Frameworks */, 62 | ); 63 | name = "Embed Frameworks"; 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXCopyFilesBuildPhase section */ 67 | 68 | /* Begin PBXFileReference section */ 69 | EA037E521A834B9300F47B17 /* Sukhasana.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sukhasana.app; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | EA037E621A834B9300F47B17 /* SukhasanaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SukhasanaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 71 | EA037E671A834B9300F47B17 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 72 | EA037E681A834B9300F47B17 /* SukhasanaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SukhasanaTests.swift; sourceTree = ""; }; 73 | EA037E791A847CF800F47B17 /* LlamaKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LlamaKit.framework; path = ../Carthage/Build/Mac/LlamaKit.framework; sourceTree = ""; }; 74 | EA037E7C1A847CF800F47B17 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveCocoa.framework; path = ../Carthage/Build/Mac/ReactiveCocoa.framework; sourceTree = ""; }; 75 | EA125B031AA40426004177C4 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 76 | EA125B041AA40426004177C4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 77 | EA125B061AA40426004177C4 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 78 | EA125B081AA40426004177C4 /* APIClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; 79 | EA125B091AA40426004177C4 /* NSUserDefaults+SettingsStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserDefaults+SettingsStore.swift"; sourceTree = ""; }; 80 | EA125B0A1AA40426004177C4 /* SignalOperators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalOperators.swift; sourceTree = ""; }; 81 | EA125B0C1AA40426004177C4 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 82 | EA125B0D1AA40426004177C4 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 83 | EA125B0E1AA40426004177C4 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 84 | EA125B0F1AA40426004177C4 /* MainViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainViewController.xib; sourceTree = ""; }; 85 | EA125B101AA40426004177C4 /* Panel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Panel.swift; sourceTree = ""; }; 86 | EA125B111AA40426004177C4 /* SeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; }; 87 | EA125B121AA40426004177C4 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 88 | EA125B131AA40426004177C4 /* SettingsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsViewController.xib; sourceTree = ""; }; 89 | EA125B141AA40426004177C4 /* ResultsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultsTableView.swift; sourceTree = ""; }; 90 | EA125B151AA40426004177C4 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 91 | EA125B171AA40426004177C4 /* ApplicationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationModel.swift; sourceTree = ""; }; 92 | EA125B181AA40426004177C4 /* MainScreenModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainScreenModel.swift; sourceTree = ""; }; 93 | EA125B191AA40426004177C4 /* SettingsScreenModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsScreenModel.swift; sourceTree = ""; }; 94 | EA307A5F1A87FC780008A566 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Alamofire.framework; path = ../Carthage/Build/Mac/Alamofire.framework; sourceTree = ""; }; 95 | EAFD89FA1ABF7A0400DA0CBE /* ResultsTableViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultsTableViewModel.swift; sourceTree = ""; }; 96 | EAFD89FC1ABF7E3100DA0CBE /* Results.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Results.swift; sourceTree = ""; }; 97 | EAFFDC7C1AACF5C400F38834 /* MASShortcut.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MASShortcut.framework; path = ../Carthage/Build/Mac/MASShortcut.framework; sourceTree = ""; }; 98 | /* End PBXFileReference section */ 99 | 100 | /* Begin PBXFrameworksBuildPhase section */ 101 | EA037E4F1A834B9300F47B17 /* Frameworks */ = { 102 | isa = PBXFrameworksBuildPhase; 103 | buildActionMask = 2147483647; 104 | files = ( 105 | EA037E7F1A847CF800F47B17 /* LlamaKit.framework in Frameworks */, 106 | EA037E851A847CF800F47B17 /* ReactiveCocoa.framework in Frameworks */, 107 | EAFFDC7D1AACF5C400F38834 /* MASShortcut.framework in Frameworks */, 108 | EA307A601A87FC780008A566 /* Alamofire.framework in Frameworks */, 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | EA037E5F1A834B9300F47B17 /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXFrameworksBuildPhase section */ 120 | 121 | /* Begin PBXGroup section */ 122 | EA037E491A834B9300F47B17 = { 123 | isa = PBXGroup; 124 | children = ( 125 | EA125B021AA40426004177C4 /* Sukhasana */, 126 | EA037E651A834B9300F47B17 /* SukhasanaTests */, 127 | EA037E531A834B9300F47B17 /* Products */, 128 | ); 129 | sourceTree = ""; 130 | }; 131 | EA037E531A834B9300F47B17 /* Products */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | EA037E521A834B9300F47B17 /* Sukhasana.app */, 135 | EA037E621A834B9300F47B17 /* SukhasanaTests.xctest */, 136 | ); 137 | name = Products; 138 | sourceTree = ""; 139 | }; 140 | EA037E651A834B9300F47B17 /* SukhasanaTests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | EA037E681A834B9300F47B17 /* SukhasanaTests.swift */, 144 | EA037E661A834B9300F47B17 /* Supporting Files */, 145 | ); 146 | path = SukhasanaTests; 147 | sourceTree = ""; 148 | }; 149 | EA037E661A834B9300F47B17 /* Supporting Files */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | EA037E671A834B9300F47B17 /* Info.plist */, 153 | ); 154 | name = "Supporting Files"; 155 | sourceTree = ""; 156 | }; 157 | EA125B021AA40426004177C4 /* Sukhasana */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | EA125B2D1AA4044A004177C4 /* Frameworks */, 161 | EA125B031AA40426004177C4 /* Images.xcassets */, 162 | EA125B041AA40426004177C4 /* Info.plist */, 163 | EA125B051AA40426004177C4 /* Model */, 164 | EA125B071AA40426004177C4 /* Other */, 165 | EA125B0B1AA40426004177C4 /* View */, 166 | EA125B161AA40426004177C4 /* ViewModel */, 167 | ); 168 | path = Sukhasana; 169 | sourceTree = ""; 170 | }; 171 | EA125B051AA40426004177C4 /* Model */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | EA125B061AA40426004177C4 /* Settings.swift */, 175 | EAFD89FC1ABF7E3100DA0CBE /* Results.swift */, 176 | ); 177 | path = Model; 178 | sourceTree = ""; 179 | }; 180 | EA125B071AA40426004177C4 /* Other */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | EA125B081AA40426004177C4 /* APIClient.swift */, 184 | EA125B091AA40426004177C4 /* NSUserDefaults+SettingsStore.swift */, 185 | EA125B0A1AA40426004177C4 /* SignalOperators.swift */, 186 | ); 187 | path = Other; 188 | sourceTree = ""; 189 | }; 190 | EA125B0B1AA40426004177C4 /* View */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | EA125B0C1AA40426004177C4 /* AppDelegate.swift */, 194 | EA125B0D1AA40426004177C4 /* MainMenu.xib */, 195 | EA125B0E1AA40426004177C4 /* MainViewController.swift */, 196 | EA125B0F1AA40426004177C4 /* MainViewController.xib */, 197 | EA125B101AA40426004177C4 /* Panel.swift */, 198 | EA125B141AA40426004177C4 /* ResultsTableView.swift */, 199 | EA125B111AA40426004177C4 /* SeparatorView.swift */, 200 | EA125B121AA40426004177C4 /* SettingsViewController.swift */, 201 | EA125B131AA40426004177C4 /* SettingsViewController.xib */, 202 | EA125B151AA40426004177C4 /* ViewController.swift */, 203 | ); 204 | path = View; 205 | sourceTree = ""; 206 | }; 207 | EA125B161AA40426004177C4 /* ViewModel */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | EA125B171AA40426004177C4 /* ApplicationModel.swift */, 211 | EA125B181AA40426004177C4 /* MainScreenModel.swift */, 212 | EAFD89FA1ABF7A0400DA0CBE /* ResultsTableViewModel.swift */, 213 | EA125B191AA40426004177C4 /* SettingsScreenModel.swift */, 214 | ); 215 | path = ViewModel; 216 | sourceTree = ""; 217 | }; 218 | EA125B2D1AA4044A004177C4 /* Frameworks */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | EA307A5F1A87FC780008A566 /* Alamofire.framework */, 222 | EA037E791A847CF800F47B17 /* LlamaKit.framework */, 223 | EAFFDC7C1AACF5C400F38834 /* MASShortcut.framework */, 224 | EA037E7C1A847CF800F47B17 /* ReactiveCocoa.framework */, 225 | ); 226 | name = Frameworks; 227 | sourceTree = ""; 228 | }; 229 | /* End PBXGroup section */ 230 | 231 | /* Begin PBXNativeTarget section */ 232 | EA037E511A834B9300F47B17 /* Sukhasana */ = { 233 | isa = PBXNativeTarget; 234 | buildConfigurationList = EA037E6C1A834B9300F47B17 /* Build configuration list for PBXNativeTarget "Sukhasana" */; 235 | buildPhases = ( 236 | EA037E4E1A834B9300F47B17 /* Sources */, 237 | EA037E4F1A834B9300F47B17 /* Frameworks */, 238 | EA037E501A834B9300F47B17 /* Resources */, 239 | EA037E871A847CF800F47B17 /* Embed Frameworks */, 240 | ); 241 | buildRules = ( 242 | ); 243 | dependencies = ( 244 | ); 245 | name = Sukhasana; 246 | productName = Sukhasana; 247 | productReference = EA037E521A834B9300F47B17 /* Sukhasana.app */; 248 | productType = "com.apple.product-type.application"; 249 | }; 250 | EA037E611A834B9300F47B17 /* SukhasanaTests */ = { 251 | isa = PBXNativeTarget; 252 | buildConfigurationList = EA037E6F1A834B9300F47B17 /* Build configuration list for PBXNativeTarget "SukhasanaTests" */; 253 | buildPhases = ( 254 | EA037E5E1A834B9300F47B17 /* Sources */, 255 | EA037E5F1A834B9300F47B17 /* Frameworks */, 256 | EA037E601A834B9300F47B17 /* Resources */, 257 | ); 258 | buildRules = ( 259 | ); 260 | dependencies = ( 261 | EA037E641A834B9300F47B17 /* PBXTargetDependency */, 262 | ); 263 | name = SukhasanaTests; 264 | productName = SukhasanaTests; 265 | productReference = EA037E621A834B9300F47B17 /* SukhasanaTests.xctest */; 266 | productType = "com.apple.product-type.bundle.unit-test"; 267 | }; 268 | /* End PBXNativeTarget section */ 269 | 270 | /* Begin PBXProject section */ 271 | EA037E4A1A834B9300F47B17 /* Project object */ = { 272 | isa = PBXProject; 273 | attributes = { 274 | LastUpgradeCheck = 0610; 275 | ORGANIZATIONNAME = "Tom Brow"; 276 | TargetAttributes = { 277 | EA037E511A834B9300F47B17 = { 278 | CreatedOnToolsVersion = 6.1.1; 279 | }; 280 | EA037E611A834B9300F47B17 = { 281 | CreatedOnToolsVersion = 6.1.1; 282 | TestTargetID = EA037E511A834B9300F47B17; 283 | }; 284 | }; 285 | }; 286 | buildConfigurationList = EA037E4D1A834B9300F47B17 /* Build configuration list for PBXProject "Sukhasana" */; 287 | compatibilityVersion = "Xcode 3.2"; 288 | developmentRegion = English; 289 | hasScannedForEncodings = 0; 290 | knownRegions = ( 291 | en, 292 | Base, 293 | ); 294 | mainGroup = EA037E491A834B9300F47B17; 295 | productRefGroup = EA037E531A834B9300F47B17 /* Products */; 296 | projectDirPath = ""; 297 | projectRoot = ""; 298 | targets = ( 299 | EA037E511A834B9300F47B17 /* Sukhasana */, 300 | EA037E611A834B9300F47B17 /* SukhasanaTests */, 301 | ); 302 | }; 303 | /* End PBXProject section */ 304 | 305 | /* Begin PBXResourcesBuildPhase section */ 306 | EA037E501A834B9300F47B17 /* Resources */ = { 307 | isa = PBXResourcesBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | EA125B1A1AA40426004177C4 /* Images.xcassets in Resources */, 311 | EA125B211AA40426004177C4 /* MainMenu.xib in Resources */, 312 | EA125B231AA40426004177C4 /* MainViewController.xib in Resources */, 313 | EA125B271AA40426004177C4 /* SettingsViewController.xib in Resources */, 314 | ); 315 | runOnlyForDeploymentPostprocessing = 0; 316 | }; 317 | EA037E601A834B9300F47B17 /* Resources */ = { 318 | isa = PBXResourcesBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | ); 322 | runOnlyForDeploymentPostprocessing = 0; 323 | }; 324 | /* End PBXResourcesBuildPhase section */ 325 | 326 | /* Begin PBXSourcesBuildPhase section */ 327 | EA037E4E1A834B9300F47B17 /* Sources */ = { 328 | isa = PBXSourcesBuildPhase; 329 | buildActionMask = 2147483647; 330 | files = ( 331 | EA125B1C1AA40426004177C4 /* Settings.swift in Sources */, 332 | EA125B281AA40426004177C4 /* ResultsTableView.swift in Sources */, 333 | EA125B241AA40426004177C4 /* Panel.swift in Sources */, 334 | EA125B251AA40426004177C4 /* SeparatorView.swift in Sources */, 335 | EA125B291AA40426004177C4 /* ViewController.swift in Sources */, 336 | EAFD89FB1ABF7A0400DA0CBE /* ResultsTableViewModel.swift in Sources */, 337 | EA125B1F1AA40426004177C4 /* SignalOperators.swift in Sources */, 338 | EA125B2C1AA40426004177C4 /* SettingsScreenModel.swift in Sources */, 339 | EA125B2A1AA40426004177C4 /* ApplicationModel.swift in Sources */, 340 | EA125B1D1AA40426004177C4 /* APIClient.swift in Sources */, 341 | EAFD89FD1ABF7E3100DA0CBE /* Results.swift in Sources */, 342 | EA125B221AA40426004177C4 /* MainViewController.swift in Sources */, 343 | EA125B201AA40426004177C4 /* AppDelegate.swift in Sources */, 344 | EA125B2B1AA40426004177C4 /* MainScreenModel.swift in Sources */, 345 | EA125B1E1AA40426004177C4 /* NSUserDefaults+SettingsStore.swift in Sources */, 346 | EA125B261AA40426004177C4 /* SettingsViewController.swift in Sources */, 347 | ); 348 | runOnlyForDeploymentPostprocessing = 0; 349 | }; 350 | EA037E5E1A834B9300F47B17 /* Sources */ = { 351 | isa = PBXSourcesBuildPhase; 352 | buildActionMask = 2147483647; 353 | files = ( 354 | EA037E691A834B9300F47B17 /* SukhasanaTests.swift in Sources */, 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | }; 358 | /* End PBXSourcesBuildPhase section */ 359 | 360 | /* Begin PBXTargetDependency section */ 361 | EA037E641A834B9300F47B17 /* PBXTargetDependency */ = { 362 | isa = PBXTargetDependency; 363 | target = EA037E511A834B9300F47B17 /* Sukhasana */; 364 | targetProxy = EA037E631A834B9300F47B17 /* PBXContainerItemProxy */; 365 | }; 366 | /* End PBXTargetDependency section */ 367 | 368 | /* Begin XCBuildConfiguration section */ 369 | EA037E6A1A834B9300F47B17 /* Debug */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ALWAYS_SEARCH_USER_PATHS = NO; 373 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 374 | CLANG_CXX_LIBRARY = "libc++"; 375 | CLANG_ENABLE_MODULES = YES; 376 | CLANG_ENABLE_OBJC_ARC = YES; 377 | CLANG_WARN_BOOL_CONVERSION = YES; 378 | CLANG_WARN_CONSTANT_CONVERSION = YES; 379 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 380 | CLANG_WARN_EMPTY_BODY = YES; 381 | CLANG_WARN_ENUM_CONVERSION = YES; 382 | CLANG_WARN_INT_CONVERSION = YES; 383 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 384 | CLANG_WARN_UNREACHABLE_CODE = YES; 385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 386 | CODE_SIGN_IDENTITY = "-"; 387 | COPY_PHASE_STRIP = NO; 388 | ENABLE_STRICT_OBJC_MSGSEND = YES; 389 | GCC_C_LANGUAGE_STANDARD = gnu99; 390 | GCC_DYNAMIC_NO_PIC = NO; 391 | GCC_OPTIMIZATION_LEVEL = 0; 392 | GCC_PREPROCESSOR_DEFINITIONS = ( 393 | "DEBUG=1", 394 | "$(inherited)", 395 | ); 396 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 397 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 398 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 399 | GCC_WARN_UNDECLARED_SELECTOR = YES; 400 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 401 | GCC_WARN_UNUSED_FUNCTION = YES; 402 | GCC_WARN_UNUSED_VARIABLE = YES; 403 | MACOSX_DEPLOYMENT_TARGET = 10.10; 404 | MTL_ENABLE_DEBUG_INFO = YES; 405 | ONLY_ACTIVE_ARCH = YES; 406 | SDKROOT = macosx; 407 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 408 | }; 409 | name = Debug; 410 | }; 411 | EA037E6B1A834B9300F47B17 /* Release */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ALWAYS_SEARCH_USER_PATHS = NO; 415 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 416 | CLANG_CXX_LIBRARY = "libc++"; 417 | CLANG_ENABLE_MODULES = YES; 418 | CLANG_ENABLE_OBJC_ARC = YES; 419 | CLANG_WARN_BOOL_CONVERSION = YES; 420 | CLANG_WARN_CONSTANT_CONVERSION = YES; 421 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 422 | CLANG_WARN_EMPTY_BODY = YES; 423 | CLANG_WARN_ENUM_CONVERSION = YES; 424 | CLANG_WARN_INT_CONVERSION = YES; 425 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 426 | CLANG_WARN_UNREACHABLE_CODE = YES; 427 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 428 | CODE_SIGN_IDENTITY = "-"; 429 | COPY_PHASE_STRIP = YES; 430 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 431 | ENABLE_NS_ASSERTIONS = NO; 432 | ENABLE_STRICT_OBJC_MSGSEND = YES; 433 | GCC_C_LANGUAGE_STANDARD = gnu99; 434 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 435 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 436 | GCC_WARN_UNDECLARED_SELECTOR = YES; 437 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 438 | GCC_WARN_UNUSED_FUNCTION = YES; 439 | GCC_WARN_UNUSED_VARIABLE = YES; 440 | MACOSX_DEPLOYMENT_TARGET = 10.10; 441 | MTL_ENABLE_DEBUG_INFO = NO; 442 | SDKROOT = macosx; 443 | }; 444 | name = Release; 445 | }; 446 | EA037E6D1A834B9300F47B17 /* Debug */ = { 447 | isa = XCBuildConfiguration; 448 | buildSettings = { 449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 450 | COMBINE_HIDPI_IMAGES = YES; 451 | FRAMEWORK_SEARCH_PATHS = ( 452 | "$(inherited)", 453 | "$(PROJECT_DIR)/Carthage/Build/Mac", 454 | "$(PROJECT_DIR)/Sukhasana", 455 | ); 456 | INFOPLIST_FILE = Sukhasana/Info.plist; 457 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | }; 460 | name = Debug; 461 | }; 462 | EA037E6E1A834B9300F47B17 /* Release */ = { 463 | isa = XCBuildConfiguration; 464 | buildSettings = { 465 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 466 | COMBINE_HIDPI_IMAGES = YES; 467 | FRAMEWORK_SEARCH_PATHS = ( 468 | "$(inherited)", 469 | "$(PROJECT_DIR)/Carthage/Build/Mac", 470 | "$(PROJECT_DIR)/Sukhasana", 471 | ); 472 | INFOPLIST_FILE = Sukhasana/Info.plist; 473 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 474 | PRODUCT_NAME = "$(TARGET_NAME)"; 475 | }; 476 | name = Release; 477 | }; 478 | EA037E701A834B9300F47B17 /* Debug */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | BUNDLE_LOADER = "$(TEST_HOST)"; 482 | COMBINE_HIDPI_IMAGES = YES; 483 | FRAMEWORK_SEARCH_PATHS = ( 484 | "$(DEVELOPER_FRAMEWORKS_DIR)", 485 | "$(inherited)", 486 | ); 487 | GCC_PREPROCESSOR_DEFINITIONS = ( 488 | "DEBUG=1", 489 | "$(inherited)", 490 | ); 491 | INFOPLIST_FILE = SukhasanaTests/Info.plist; 492 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 493 | PRODUCT_NAME = "$(TARGET_NAME)"; 494 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sukhasana.app/Contents/MacOS/Sukhasana"; 495 | }; 496 | name = Debug; 497 | }; 498 | EA037E711A834B9300F47B17 /* Release */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | BUNDLE_LOADER = "$(TEST_HOST)"; 502 | COMBINE_HIDPI_IMAGES = YES; 503 | FRAMEWORK_SEARCH_PATHS = ( 504 | "$(DEVELOPER_FRAMEWORKS_DIR)", 505 | "$(inherited)", 506 | ); 507 | INFOPLIST_FILE = SukhasanaTests/Info.plist; 508 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 509 | PRODUCT_NAME = "$(TARGET_NAME)"; 510 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sukhasana.app/Contents/MacOS/Sukhasana"; 511 | }; 512 | name = Release; 513 | }; 514 | /* End XCBuildConfiguration section */ 515 | 516 | /* Begin XCConfigurationList section */ 517 | EA037E4D1A834B9300F47B17 /* Build configuration list for PBXProject "Sukhasana" */ = { 518 | isa = XCConfigurationList; 519 | buildConfigurations = ( 520 | EA037E6A1A834B9300F47B17 /* Debug */, 521 | EA037E6B1A834B9300F47B17 /* Release */, 522 | ); 523 | defaultConfigurationIsVisible = 0; 524 | defaultConfigurationName = Release; 525 | }; 526 | EA037E6C1A834B9300F47B17 /* Build configuration list for PBXNativeTarget "Sukhasana" */ = { 527 | isa = XCConfigurationList; 528 | buildConfigurations = ( 529 | EA037E6D1A834B9300F47B17 /* Debug */, 530 | EA037E6E1A834B9300F47B17 /* Release */, 531 | ); 532 | defaultConfigurationIsVisible = 0; 533 | defaultConfigurationName = Release; 534 | }; 535 | EA037E6F1A834B9300F47B17 /* Build configuration list for PBXNativeTarget "SukhasanaTests" */ = { 536 | isa = XCConfigurationList; 537 | buildConfigurations = ( 538 | EA037E701A834B9300F47B17 /* Debug */, 539 | EA037E711A834B9300F47B17 /* Release */, 540 | ); 541 | defaultConfigurationIsVisible = 0; 542 | defaultConfigurationName = Release; 543 | }; 544 | /* End XCConfigurationList section */ 545 | }; 546 | rootObject = EA037E4A1A834B9300F47B17 /* Project object */; 547 | } 548 | -------------------------------------------------------------------------------- /Sukhasana.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sukhasana.xcodeproj/xcshareddata/xcschemes/Sukhasana.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon-16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon-16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon-32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon-32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon-128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon-128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon-256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon-256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon-512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon-512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-128.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-128@2x.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-16.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-16@2x.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-256.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-256@2x.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-32.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-32@2x.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-512.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/AppIcon.appiconset/icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-512@2x.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/Gear.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "Gear@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/Gear.imageset/Gear@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/Gear.imageset/Gear@2x.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/StatusItemIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "StatusItemIcon.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "StatusItemIcon@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/StatusItemIcon.imageset/StatusItemIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/StatusItemIcon.imageset/StatusItemIcon.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/StatusItemIcon.imageset/StatusItemIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/StatusItemIcon.imageset/StatusItemIcon@2x.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "menu-white.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "menu-white@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/menu-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/menu-white.png -------------------------------------------------------------------------------- /Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/menu-white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brow/Sukhasana/f851382925e0b2867420b79ca1070dcaa2c2d8f5/Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/menu-white@2x.png -------------------------------------------------------------------------------- /Sukhasana/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.tombrow.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.1.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 3 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2015 Tom Brow. All rights reserved. 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /Sukhasana/Model/Results.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Results.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 3/22/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Results { 12 | let projects, tasks: [Result] 13 | 14 | static var empty: Results { 15 | return Results(projects: [], tasks: []) 16 | } 17 | } 18 | 19 | struct Result { 20 | let name, id: String 21 | var URL: NSURL { 22 | // FIXME: escape id 23 | return NSURL(string: "https://app.asana.com/0/\(id)/\(id)")! 24 | } 25 | } -------------------------------------------------------------------------------- /Sukhasana/Model/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/21/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | struct Settings { 10 | let APIKey, workspaceID: String 11 | } -------------------------------------------------------------------------------- /Sukhasana/Other/APIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/16/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | import Alamofire 11 | 12 | struct APIClient { 13 | init(APIKey: String) { 14 | let config = NSURLSessionConfiguration.defaultSessionConfiguration() 15 | config.HTTPAdditionalHeaders = ["Authorization": "Basic " + ("\(APIKey):" as NSString) 16 | .dataUsingEncoding(NSUTF8StringEncoding)! 17 | .base64EncodedStringWithOptions(NSDataBase64EncodingOptions.allZeros)] 18 | 19 | requestManager = Manager(configuration: config) 20 | } 21 | 22 | enum TypeaheadType: String { 23 | case Task = "task" 24 | case Project = "project" 25 | } 26 | 27 | func requestTypeaheadResultsInWorkspace(workspaceID: String, ofType type: TypeaheadType, matchingQuery query: String) -> SignalProducer { 28 | let typeaheadType = "task" 29 | 30 | return SignalProducer { observer, _ in 31 | self.requestManager 32 | .request( 33 | .GET, 34 | // FIXME: escape workspaceID 35 | "https://app.asana.com/api/1.0/workspaces/\(workspaceID)/typeahead", 36 | parameters: ["type": type.rawValue, "query": query, "count": 10]) 37 | .validate() 38 | .responseJSON {_, _, JSON, error in 39 | if let error = error { 40 | sendError(observer, error) 41 | } else { 42 | if let dict = JSON as? NSDictionary { 43 | sendNext(observer, dict) 44 | } 45 | sendCompleted(observer) 46 | } 47 | } 48 | return 49 | } 50 | } 51 | 52 | func requestWorkspaces() -> SignalProducer { 53 | return SignalProducer { observer, _ in 54 | self.requestManager 55 | .request( 56 | .GET, 57 | "https://app.asana.com/api/1.0/workspaces") 58 | .validate() 59 | .responseJSON {_, _, JSON, error in 60 | if let error = error { 61 | sendError(observer, error) 62 | } else { 63 | if let dict = JSON as? NSDictionary { 64 | sendNext(observer, dict) 65 | } 66 | sendCompleted(observer) 67 | } 68 | } 69 | return 70 | } 71 | } 72 | 73 | // MARK: private 74 | private let requestManager: Alamofire.Manager 75 | } 76 | -------------------------------------------------------------------------------- /Sukhasana/Other/NSUserDefaults+SettingsStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSUserDefaults+SettingsStore.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/21/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let APIKeyKey = "APIKey", workspaceIDKey = "workspaceID" 12 | 13 | extension NSUserDefaults: SettingsStore { 14 | 15 | func saveSettings(settings: Settings) -> () { 16 | setObject(settings.APIKey, forKey: APIKeyKey) 17 | setObject(settings.workspaceID, forKey: workspaceIDKey) 18 | } 19 | 20 | func restoreSettings() -> Settings? { 21 | if let APIKey = stringForKey(APIKeyKey) { 22 | if let workspaceID = stringForKey(workspaceIDKey) { 23 | return Settings(APIKey: APIKey, workspaceID: workspaceID) 24 | } 25 | } 26 | return nil 27 | } 28 | } -------------------------------------------------------------------------------- /Sukhasana/Other/SignalOperators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignalOperators.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/16/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | func startWith(value: T)(producer: ReactiveCocoa.SignalProducer) -> ReactiveCocoa.SignalProducer { 12 | return SignalProducer(value: value) |> concat(producer) 13 | } 14 | 15 | func catchTo(value: T)(producer: ReactiveCocoa.SignalProducer) -> ReactiveCocoa.SignalProducer { 16 | return catch({ _ in SignalProducer(value: value) })(producer: producer) 17 | } 18 | 19 | func propertyOf(initialValue: T, producer: SignalProducer) -> PropertyOf { 20 | let mutableProperty = MutableProperty(initialValue) 21 | mutableProperty <~ producer 22 | return PropertyOf(mutableProperty) 23 | } 24 | 25 | func replay(capacity: Int = Int.max)(producer: ReactiveCocoa.SignalProducer) -> ReactiveCocoa.SignalProducer { 26 | let (returnedProducer, sink) = SignalProducer.buffer(capacity) 27 | producer.start(sink) 28 | return returnedProducer 29 | } 30 | 31 | func mapOptional(transform: T -> U?)(signal: Signal) -> Signal { 32 | return signal |> map(transform) |> filter { $0 != nil } |> map { $0! } 33 | } -------------------------------------------------------------------------------- /Sukhasana/View/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/4/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import MASShortcut 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, MainViewControllerDelegate, NSApplicationDelegate, NSWindowDelegate { 14 | @IBOutlet var panel: NSPanel! 15 | 16 | deinit { 17 | notificationCenter.removeObserver(statusItemMoveObserver) 18 | } 19 | 20 | @IBAction func didClickStatusItem(sender: AnyObject) { 21 | if panel.keyWindow { 22 | panel.close() 23 | } else { 24 | // Later in this event loop iteration, the highlight of the status item 25 | // button is automatically set to false. So if we want to highlight the 26 | // button, as we do in `windowDidBecomeKey`, we need to ensure that happens 27 | // in a later iteration. 28 | dispatch_async(dispatch_get_main_queue()) { 29 | self.panel.makeKeyAndOrderFront(self) 30 | } 31 | } 32 | } 33 | 34 | // MARK: MainViewControllerDelegate 35 | 36 | func mainViewControllerDidChangeFittingSize(mainViewController: MainViewController) { 37 | updatePanelFrame() 38 | } 39 | 40 | // MARK: NSWindowDelegate 41 | 42 | func windowDidResignKey(notification: NSNotification) { 43 | // windowDidResignKey is always called when the window is closed (e.g., by 44 | // calling panel.close()), so it's a reliable place to un-highlight the 45 | // status item. 46 | statusItem.button?.highlight(false) 47 | 48 | // However, the window isn't necessarily closed when it resigns key, so we 49 | // must ensure here that it closes. 50 | panel.close() 51 | } 52 | 53 | func windowDidBecomeKey(notification: NSNotification) { 54 | displayingViewController?.viewDidDisplay() 55 | self.statusItem.button?.highlight(true) 56 | } 57 | 58 | // MARK: NSApplicationDelegate 59 | 60 | func applicationDidFinishLaunching(notification: NSNotification) { 61 | statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-2 /* NSSquareStatusItemLength */) 62 | if let button = statusItem.button { 63 | button.image = NSImage(named: "StatusItemIcon") 64 | button.alternateImage = NSImage(named: "StatusItemIconHighlighted") 65 | button.target = self 66 | button.action = "didClickStatusItem:" 67 | } 68 | 69 | panel.floatingPanel = true 70 | 71 | // The status item can move when other items are removed or hidden. 72 | statusItemMoveObserver = notificationCenter.addObserverForName( 73 | NSWindowDidMoveNotification, 74 | object: statusItem.button!.window!, 75 | queue: nil, 76 | usingBlock: { [weak self] _ in self?.updatePanelFrame(); return }) 77 | 78 | model.effects.start { [weak self] effect in 79 | if let _self = self { 80 | switch effect { 81 | 82 | case .DisplayScreen(let screen): 83 | _self.displayingViewController = { 84 | switch screen { 85 | case .Settings(let model): 86 | return SettingsViewController(model: model) 87 | case .Main(let model): 88 | return MainViewController(model: model, delegate: _self) 89 | } 90 | }() 91 | _self.setContentView(_self.displayingViewController!.view) 92 | _self.displayingViewController!.viewDidDisplay() 93 | 94 | case .Results(.OpenURL(let URL)): 95 | NSWorkspace.sharedWorkspace().openURL(URL) 96 | _self.panel.close() 97 | 98 | case .Results(.WriteObjectsToPasteboard(let objects)): 99 | let pasteboard = NSPasteboard.generalPasteboard() 100 | pasteboard.clearContents() 101 | pasteboard.writeObjects(objects) 102 | _self.panel.close() 103 | } 104 | } 105 | } 106 | 107 | if model.shouldOpenPanelOnLaunch { 108 | panel.makeKeyAndOrderFront(self) 109 | } 110 | 111 | MASShortcutBinder.sharedBinder().bindShortcutWithDefaultsKey(globalShortcutDefaultsKey) { [weak self] in 112 | if let _self = self { 113 | _self.panel.makeKeyAndOrderFront(_self) 114 | } 115 | } 116 | } 117 | 118 | // MARK: private 119 | 120 | private func setContentView(view: NSView) { 121 | panel.contentView = view 122 | updatePanelFrame() 123 | } 124 | 125 | private func updatePanelFrame() { 126 | if let statusItemWindow = statusItem.button?.window? { 127 | let statusItemFrame = statusItemWindow.frame 128 | let panelSize = panel.contentView.fittingSize 129 | let maxPanelOriginX: CGFloat? = { 130 | if let screen = statusItemWindow.screen { 131 | let frame = screen.visibleFrame 132 | return frame.origin.x + frame.size.width - panelSize.width 133 | } else { 134 | return nil 135 | } 136 | }() 137 | let panelOrigin = CGPoint( 138 | x: min(statusItemFrame.origin.x, maxPanelOriginX ?? CGFloat.max), 139 | y: statusItemFrame.origin.y + statusItemFrame.size.height) 140 | let panelFrame = NSMakeRect( 141 | panelOrigin.x, 142 | panelOrigin.y - panelSize.height, 143 | panelSize.width, 144 | panelSize.height) 145 | 146 | panel.setFrame(panelFrame, display: true) 147 | } 148 | } 149 | 150 | private let model = ApplicationModel( 151 | settingsStore: NSUserDefaults.standardUserDefaults(), 152 | globalShortcutDefaultsKey: globalShortcutDefaultsKey) 153 | private let notificationCenter = NSNotificationCenter.defaultCenter() 154 | private var statusItem: NSStatusItem! 155 | private var statusItemMoveObserver: AnyObject! 156 | private var displayingViewController: ViewController? 157 | } 158 | 159 | private let globalShortcutDefaultsKey = "globalShortcut" 160 | 161 | -------------------------------------------------------------------------------- /Sukhasana/View/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | Default 532 | 533 | 534 | 535 | 536 | 537 | 538 | Left to Right 539 | 540 | 541 | 542 | 543 | 544 | 545 | Right to Left 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | Default 557 | 558 | 559 | 560 | 561 | 562 | 563 | Left to Right 564 | 565 | 566 | 567 | 568 | 569 | 570 | Right to Left 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | -------------------------------------------------------------------------------- /Sukhasana/View/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/12/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ReactiveCocoa 11 | 12 | class MainViewController: NSViewController, ViewController, NSTextFieldDelegate { 13 | @IBOutlet var textField: NSTextField! 14 | @IBOutlet var resultsTableView: ResultsTableView! 15 | @IBOutlet var resultsTableViewHeightConstraint: NSLayoutConstraint! 16 | @IBOutlet var progressIndicator: NSProgressIndicator! 17 | 18 | init?(model: MainScreenModel, delegate: MainViewControllerDelegate) { 19 | self.model = model 20 | self.delegate = delegate 21 | 22 | super.init(nibName: "MainViewController", bundle: nil) 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError("not supported") 27 | } 28 | 29 | @IBAction func didClickSettingsButton(sender: AnyObject?) { 30 | sendNext(model.didClickSettingsButton, ()) 31 | } 32 | 33 | // MARK: ViewController 34 | 35 | func viewDidDisplay() { 36 | view.window?.makeFirstResponder(textField) 37 | } 38 | 39 | // MARK: NSTextFieldDelegate 40 | 41 | override func controlTextDidChange(obj: NSNotification) { 42 | model.textFieldText.put(textField!.stringValue) 43 | } 44 | 45 | func control(control: NSControl, textView: NSTextView, doCommandBySelector commandSelector: Selector) -> Bool { 46 | switch commandSelector { 47 | case "moveDown:": 48 | // Down arrow key moves from search field to results table 49 | control.window?.selectKeyViewFollowingView(control) 50 | return true 51 | default: 52 | return false 53 | } 54 | } 55 | 56 | // MARK: NSViewController 57 | 58 | override func loadView() { 59 | super.loadView() 60 | 61 | resultsTableView.model <~ model.resultsTableViewModel 62 | 63 | resultsTableView.fittingHeight.start { [weak self] height in 64 | if let _self = self { 65 | _self.resultsTableViewHeightConstraint.constant = height 66 | _self.delegate?.mainViewControllerDidChangeFittingSize(_self) 67 | } 68 | } 69 | 70 | model.activityIndicatorIsAnimating.producer.start { [weak self] animating in 71 | if animating { 72 | self?.progressIndicator.startAnimation(self) 73 | } else { 74 | self?.progressIndicator.stopAnimation(self) 75 | } 76 | } 77 | } 78 | 79 | // MARK: private 80 | 81 | private let model: MainScreenModel 82 | private weak var delegate: MainViewControllerDelegate? 83 | } 84 | 85 | protocol MainViewControllerDelegate: NSObjectProtocol { 86 | func mainViewControllerDidChangeFittingSize(mainViewController: MainViewController) 87 | } -------------------------------------------------------------------------------- /Sukhasana/View/MainViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 91 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 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 | -------------------------------------------------------------------------------- /Sukhasana/View/Panel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Panel.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/7/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class Panel: NSPanel { 12 | // MARK: NSWindow 13 | 14 | override var canBecomeKeyWindow: Bool { 15 | return true 16 | } 17 | 18 | override func recalculateKeyViewLoop() { 19 | // If this is not overridden, then the nextKeyView outlets set in the nibs 20 | // are ignored the first time that a given view is displayed in the panel. 21 | // This is not helped by unsetting NSPanel.autorecalculatesKeyViewLoop. 22 | } 23 | 24 | // MARK: NSResponder 25 | 26 | func cancel(sender: AnyObject?) { 27 | // Called during Escape or Command-. shortcuts. 28 | close() 29 | } 30 | } -------------------------------------------------------------------------------- /Sukhasana/View/ResultsTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableView.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/22/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Carbon 11 | import ReactiveCocoa 12 | 13 | class ResultsTableView: NSTableView, NSTableViewDataSource, NSTableViewDelegate { 14 | let model: MutableProperty 15 | let fittingHeight: SignalProducer 16 | 17 | required init?(coder: NSCoder) { 18 | model = MutableProperty(ResultsTableViewModel.makeWithResults(Results.empty).0) 19 | 20 | fittingHeight = model.producer 21 | |> map { model in 22 | let numberOfRows = model.numberOfRows() 23 | let bottomPadding = CGFloat(numberOfRows > 0 ? 4 : 0) 24 | return bottomPadding + 25 | map(0..= 0 { 51 | // For some reason, the flashHighlightedRowsThen animation's timing is off 52 | // if initiated in the same event loop iteration `self.copy` is called. 53 | dispatch_async(dispatch_get_main_queue()) { 54 | self.didRecognizeAction(.Copy, onRowAtIndex: self.selectedRow) 55 | } 56 | } 57 | } 58 | 59 | func didRecognizeAction(action: ResultsTableViewModel.Action, onRowAtIndex index: Int) { 60 | flashHighlightedRowsThen { 61 | sendNext(self.model.value.didRecognizeActionOnRowAtIndex, (action, index)) 62 | } 63 | } 64 | 65 | // MARK: NSTableViewDataSource 66 | 67 | func numberOfRowsInTableView(tableView: NSTableView) -> Int { 68 | return model.value.numberOfRows() 69 | } 70 | 71 | // MARK: NSTableViewDelegate 72 | 73 | func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? { 74 | switch model.value.cellForRow(row) { 75 | case .Selectable(let text): 76 | let view = tableView.makeViewWithIdentifier("SelectableCell", owner: self) as NSTableCellView 77 | view.textField?.stringValue = text 78 | return view 79 | case .Separator: 80 | return tableView.makeViewWithIdentifier("SeparatorView", owner: self) as? NSView 81 | } 82 | } 83 | 84 | func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool { 85 | switch model.value.cellForRow(row) { 86 | case .Selectable: 87 | return true 88 | case .Separator: 89 | return false 90 | } 91 | } 92 | 93 | func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat { 94 | return heightForCell(model.value.cellForRow(row)) 95 | } 96 | 97 | // MARK: NSResponder 98 | 99 | func acceptsFirstResponder() -> Bool { 100 | return numberOfRows > 0 101 | } 102 | 103 | override func becomeFirstResponder() -> Bool { 104 | selectRowIndexes(NSIndexSet(index: 0), byExtendingSelection: false) 105 | return true 106 | } 107 | 108 | override func mouseExited(theEvent: NSEvent) { 109 | super.mouseExited(theEvent) 110 | selectRowIndexes(NSIndexSet(), byExtendingSelection: false) 111 | } 112 | 113 | override func mouseMoved(theEvent: NSEvent) { 114 | super.mouseMoved(theEvent) 115 | 116 | let row = rowAtPoint(convertPoint(theEvent.locationInWindow, fromView: nil)) 117 | let rowIndexesToSelect = row == -1 118 | ? NSIndexSet() 119 | : NSIndexSet(index: row) 120 | 121 | window?.makeFirstResponder(self) 122 | selectRowIndexes(rowIndexesToSelect, byExtendingSelection: false) 123 | } 124 | 125 | override func mouseDown(theEvent: NSEvent) { 126 | super.mouseDown(theEvent) 127 | 128 | let row = rowAtPoint(convertPoint(theEvent.locationInWindow, fromView: nil)) 129 | if row >= 0 { 130 | didRecognizeAction(.Click, onRowAtIndex: row) 131 | } 132 | } 133 | 134 | override func keyDown(theEvent: NSEvent) { 135 | switch Int(theEvent.keyCode) { 136 | case kVK_UpArrow where selectedRow == 0: 137 | window?.selectKeyViewPrecedingView(self) 138 | case kVK_Space, kVK_Return where selectedRow >= 0: 139 | didRecognizeAction(.Click, onRowAtIndex: selectedRow) 140 | default: 141 | super.keyDown(theEvent) 142 | } 143 | } 144 | 145 | override func resignFirstResponder() -> Bool { 146 | if super.resignFirstResponder() { 147 | deselectAll(self) 148 | return true 149 | } else { 150 | return false 151 | } 152 | } 153 | } 154 | 155 | private func heightForCell(cell: ResultsTableViewModel.Cell) -> CGFloat { 156 | switch cell { 157 | case .Selectable: 158 | return 22 159 | case .Separator: 160 | return 1 161 | } 162 | } 163 | 164 | extension NSTableView { 165 | func flashHighlightedRowsThen(callback: () -> ()) { 166 | // Simulate NSMenu's flash on selecting a row, not without jank 167 | let baseHighlightStyle = selectionHighlightStyle 168 | let mainQueue = dispatch_get_main_queue() 169 | let toggleInterval = Int64(NSEC_PER_SEC) / 20 170 | let numberOfToggles: Int64 = 3 171 | 172 | for i: Int64 in 0...numberOfToggles { 173 | let time = dispatch_time(DISPATCH_TIME_NOW, toggleInterval * i) 174 | dispatch_after(time, mainQueue) { 175 | if i < numberOfToggles { 176 | self.selectionHighlightStyle = i % 2 == 0 177 | ? NSTableViewSelectionHighlightStyle.None 178 | : baseHighlightStyle 179 | } else { 180 | self.selectionHighlightStyle = baseHighlightStyle 181 | callback() 182 | } 183 | } 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /Sukhasana/View/SeparatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeparatorView.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/22/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SeparatorView: NSView { 12 | override func drawRect(dirtyRect: NSRect) { 13 | super.drawRect(dirtyRect) 14 | NSColor(calibratedWhite: 0.8, alpha: 1).setFill() 15 | NSRectFill(dirtyRect) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sukhasana/View/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/16/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ReactiveCocoa 11 | import MASShortcut 12 | 13 | class SettingsViewController: NSViewController, ViewController, NSTextFieldDelegate { 14 | @IBOutlet var APIKeyTextField: NSTextField! 15 | @IBOutlet var workspacePopUpButton: NSPopUpButton! 16 | @IBOutlet var saveButton: NSButton! 17 | @IBOutlet var progressIndicator: NSProgressIndicator! 18 | @IBOutlet var shortcutView: MASShortcutView! 19 | 20 | init?(model: SettingsScreenModel) { 21 | self.model = model 22 | 23 | super.init(nibName: "SettingsViewController", bundle: nil) 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | fatalError("not supported") 28 | } 29 | 30 | @IBAction func didClickSaveButton(sender: AnyObject?) { 31 | sendNext(model.didClickSaveButton, ()) 32 | } 33 | 34 | @IBAction func workspacePopUpButtonDidSelectItem(sender: AnyObject?) { 35 | sendNext(model.workspacePopUpDidSelectItemAtIndex, workspacePopUpButton.indexOfSelectedItem) 36 | } 37 | 38 | // MARK: ViewController 39 | 40 | func viewDidDisplay() { 41 | view.window?.makeFirstResponder(APIKeyTextField) 42 | } 43 | 44 | // MARK: NSTextFieldDelegate 45 | 46 | override func controlTextDidChange(obj: NSNotification) { 47 | model.APIKeyTextFieldText.put(APIKeyTextField.stringValue) 48 | } 49 | 50 | // MARK: NSViewController 51 | 52 | override func loadView() { 53 | super.loadView() 54 | 55 | model.saveButtonEnabled.producer.start { [weak self] enabled in 56 | self?.saveButton?.enabled = enabled 57 | return 58 | } 59 | model.progressIndicatorAnimating.producer.start { [weak self] animating in 60 | if animating { 61 | self?.progressIndicator.startAnimation(nil) 62 | } else { 63 | self?.progressIndicator.stopAnimation(nil) 64 | } 65 | } 66 | model.workspacePopUpButtonEnabled.producer.start { [weak self] enabled in 67 | self?.workspacePopUpButton?.enabled = enabled 68 | return 69 | } 70 | model.workspacePopUpItemsTitles.producer.start { [weak self] titles in 71 | self?.workspacePopUpButton.removeAllItems() 72 | self?.workspacePopUpButton.addItemsWithTitles(titles) 73 | } 74 | 75 | APIKeyTextField.stringValue = model.APIKeyTextFieldText.value 76 | workspacePopUpButton.selectItemAtIndex(model.workspacePopupSelectedIndex.value) 77 | shortcutView.associatedUserDefaultsKey = model.shortcutViewAssociatedUserDefaultsKey 78 | } 79 | 80 | // MARK: private 81 | 82 | private let model: SettingsScreenModel 83 | } 84 | -------------------------------------------------------------------------------- /Sukhasana/View/SettingsViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /Sukhasana/View/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/21/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol ViewController { 12 | var view: NSView { get } 13 | func viewDidDisplay() 14 | } 15 | -------------------------------------------------------------------------------- /Sukhasana/ViewModel/ApplicationModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationModel.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/16/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | struct ApplicationModel { 12 | enum Screen { 13 | case Settings(SettingsScreenModel) 14 | case Main(MainScreenModel) 15 | } 16 | 17 | enum Effect { 18 | case DisplayScreen(Screen) 19 | case Results(ResultsTableViewModel.Effect) 20 | } 21 | 22 | let effects: SignalProducer 23 | let shouldOpenPanelOnLaunch: Bool 24 | 25 | init(settingsStore: SettingsStore, globalShortcutDefaultsKey: String) { 26 | let restoredSettings = settingsStore.restoreSettings() 27 | let (settingsModel, didSaveSettings) = SettingsScreenModel.makeWithSettings( 28 | restoredSettings, 29 | globalShortcutDefaultsKey: globalShortcutDefaultsKey) 30 | 31 | // Persist saved settings 32 | didSaveSettings.start { settings in 33 | settingsStore.saveSettings(settings) 34 | } 35 | 36 | // Show the main screen right away if settings already exist 37 | let didRestoreSettings: SignalProducer = { 38 | if let restoredSettings = restoredSettings { 39 | return SignalProducer(value: restoredSettings) 40 | } else { 41 | return SignalProducer.empty 42 | } 43 | }() 44 | 45 | // Show the main screen after new settings are saved 46 | let mainModelsAndEffects = didRestoreSettings 47 | |> concat(didSaveSettings) 48 | |> map(MainScreenModel.makeWithSettings) 49 | |> replay(capacity: 1) 50 | let shouldDisplayMainScreen = mainModelsAndEffects 51 | |> map { Screen.Main($0.0) } 52 | 53 | // Show the settings screen after the Settings button is clicked, and on 54 | // launch if no settings have been restored 55 | let shouldDisplaySettingsScreen = SignalProducer(values: restoredSettings == nil ? [()] : []) 56 | |> concat ( 57 | mainModelsAndEffects 58 | |> joinMap(.Latest) { $0.effects } 59 | |> filter { 60 | switch $0 { 61 | case .OpenSettings: 62 | return true 63 | default: 64 | return false 65 | } 66 | } 67 | |> map { _ in () } 68 | ) 69 | |> map { _ in Screen.Settings(settingsModel) } 70 | 71 | // If settings haven't been entered yet, this is probably the first launch. 72 | // We should open the panel to show the user where it is. 73 | shouldOpenPanelOnLaunch = restoredSettings == nil 74 | 75 | effects = SignalProducer(values: [ 76 | shouldDisplayMainScreen 77 | |> map { .DisplayScreen($0) }, 78 | shouldDisplaySettingsScreen 79 | |> map { .DisplayScreen($0) }, 80 | mainModelsAndEffects 81 | |> joinMap(.Latest) { $0.effects} 82 | |> mapOptional { effect in 83 | switch effect { 84 | case .Results(let effect): 85 | return .Results(effect) 86 | default: 87 | return nil 88 | } 89 | }, 90 | ]) 91 | |> join(.Merge) 92 | } 93 | } 94 | 95 | protocol SettingsStore { 96 | func saveSettings(settings: Settings) -> () 97 | func restoreSettings() -> Settings? 98 | } 99 | -------------------------------------------------------------------------------- /Sukhasana/ViewModel/MainScreenModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainScreenModel.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/8/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | struct MainScreenModel { 12 | enum Effect { 13 | case OpenSettings 14 | case Results(ResultsTableViewModel.Effect) 15 | } 16 | 17 | let textFieldText = MutableProperty("") 18 | let activityIndicatorIsAnimating: PropertyOf 19 | let resultsTableViewModel: PropertyOf 20 | let didClickSettingsButton: Signal<(), NoError>.Observer 21 | 22 | static func makeWithSettings(settings: Settings) -> ( 23 | MainScreenModel, 24 | effects: SignalProducer) 25 | { 26 | let (effects, effectsSink) = SignalProducer.buffer(1) 27 | 28 | return ( 29 | MainScreenModel( 30 | settings: settings, 31 | effectsSink: effectsSink), 32 | effects: effects 33 | ) 34 | } 35 | 36 | // MARK: private 37 | 38 | private init( 39 | settings: Settings, 40 | effectsSink: Signal.Observer) 41 | { 42 | let client = APIClient(APIKey: settings.APIKey) 43 | 44 | let fetchState: SignalProducer = textFieldText.producer 45 | |> map { query -> SignalProducer in 46 | if query == "" { 47 | // The empty query always returns no results, so don't bother 48 | return SignalProducer(value: Results(projects: [], tasks: [])) 49 | } else { 50 | let request = { type in 51 | client.requestTypeaheadResultsInWorkspace(settings.workspaceID, ofType: type, matchingQuery: query) 52 | |> map(resultsFromJSON) 53 | } 54 | return zip(request(.Project), request(.Task)) 55 | |> map { projects, tasks in Results(projects: projects, tasks: tasks) } 56 | } 57 | } 58 | |> map { request in 59 | request 60 | |> map { results in .Fetched(results) } 61 | |> catchTo(.Failed) 62 | |> startWith(.Fetching) 63 | } 64 | |> join(.Latest) 65 | |> replay(capacity: 1) 66 | 67 | let resultsTableViewModelsAndEffects: SignalProducer<(ResultsTableViewModel, SignalProducer), NoError> = 68 | fetchState 69 | |> map { fetchState in 70 | switch fetchState { 71 | case .Fetched(let results): 72 | return ResultsTableViewModel.makeWithResults(results) 73 | case .Initial, .Failed, .Fetching: 74 | return ResultsTableViewModel.makeWithResults(Results.empty) 75 | } 76 | } 77 | |> replay(capacity: 1) 78 | 79 | resultsTableViewModel = propertyOf( 80 | ResultsTableViewModel.makeWithResults(Results.empty).0, 81 | resultsTableViewModelsAndEffects |> map { $0.0 } ) 82 | 83 | resultsTableViewModelsAndEffects 84 | |> joinMap(.Latest) { $0.1 } 85 | |> map { .Results($0) } 86 | |> start(effectsSink) 87 | 88 | activityIndicatorIsAnimating = propertyOf(false, fetchState 89 | |> map { resultsState in 90 | switch resultsState { 91 | case .Fetching: 92 | return true 93 | case .Initial, .Failed, .Fetched: 94 | return false 95 | } 96 | }) 97 | 98 | didClickSettingsButton = { 99 | let buffer = SignalProducer<(), NoError>.buffer(1) 100 | buffer.0 101 | |> map { _ in .OpenSettings} 102 | |> start(effectsSink) 103 | return buffer.1 104 | }() 105 | } 106 | } 107 | 108 | private enum FetchState { 109 | case Initial 110 | case Fetching 111 | case Failed 112 | case Fetched(Results) 113 | } 114 | 115 | private func resultsFromJSON(JSON: NSDictionary) -> [Result] { 116 | var names = [Result]() 117 | if let data = JSON["data"] as? Array> { 118 | for object in data { 119 | if let name = object["name"] as? String { 120 | if let id = object["id"] as? NSNumber { 121 | names.append(Result(name: name, id: id.stringValue)) 122 | } 123 | } 124 | } 125 | } 126 | return names 127 | } 128 | 129 | -------------------------------------------------------------------------------- /Sukhasana/ViewModel/ResultsTableViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResultsScreenModel.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 3/22/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ReactiveCocoa 11 | 12 | struct ResultsTableViewModel { 13 | enum Cell { 14 | case Selectable(String) 15 | case Separator 16 | } 17 | 18 | enum Action { 19 | case Click 20 | case Copy 21 | } 22 | 23 | enum Effect { 24 | case OpenURL(NSURL) 25 | case WriteObjectsToPasteboard([NSPasteboardWriting]) 26 | } 27 | 28 | let didRecognizeActionOnRowAtIndex: Signal<(Action, Int), NoError>.Observer 29 | 30 | static func makeWithResults(results: Results) -> ( 31 | ResultsTableViewModel, 32 | effects: SignalProducer) 33 | { 34 | let (effects, effectsSink) = SignalProducer.buffer(1) 35 | 36 | return ( 37 | ResultsTableViewModel( 38 | results: results, 39 | effectsSink: effectsSink), 40 | effects: effects) 41 | } 42 | 43 | func numberOfRows() -> Int { 44 | return countElements(resultsTable.rows) 45 | } 46 | 47 | func cellForRow(row: Int) -> Cell { 48 | switch resultsTable.rows[row] { 49 | case let .Item(text: text, clickURL: _): 50 | return .Selectable(text) 51 | case .Separator: 52 | return .Separator 53 | } 54 | } 55 | 56 | // MARK: private 57 | 58 | private let resultsTable: ResultsTable 59 | 60 | private init(results: Results, effectsSink: Signal.Observer) { 61 | let resultsTable = ResultsTable(results: results) 62 | self.resultsTable = resultsTable 63 | 64 | didRecognizeActionOnRowAtIndex = { 65 | let buffer = SignalProducer<(Action, Int), NoError>.buffer(1) 66 | buffer.0 67 | |> mapOptional { (action, index) in 68 | switch resultsTable.rows[index] { 69 | case let .Item(text: text, clickURL: clickURL): 70 | switch action { 71 | case .Click: 72 | return .OpenURL(clickURL) 73 | case .Copy: 74 | let pasteboardItem = pasteboardItemForLinkWithText(text, URL: clickURL) 75 | return .WriteObjectsToPasteboard([pasteboardItem]) 76 | } 77 | case .Separator: 78 | return nil 79 | } 80 | } 81 | |> start(effectsSink) 82 | return buffer.1 83 | }() 84 | 85 | } 86 | } 87 | 88 | private struct ResultsTable { 89 | enum Row { 90 | case Item(text: String, clickURL: NSURL) 91 | case Separator 92 | } 93 | 94 | let rows: [Row] 95 | 96 | init () { 97 | rows = [] 98 | } 99 | 100 | init(results: Results) { 101 | let sections: [[Row]] = [results.projects, results.tasks] 102 | .filter { !$0.isEmpty } 103 | .map { section in 104 | section.map { result in 105 | .Item( 106 | text: result.name.stringByReplacingOccurrencesOfString("\n", withString: "⏎"), 107 | clickURL: result.URL) 108 | }} 109 | 110 | rows = [.Separator].join(sections) 111 | } 112 | } 113 | 114 | private func pasteboardItemForLinkWithText(text: String, #URL: NSURL) -> NSPasteboardItem { 115 | let hyperlink = NSAttributedString( 116 | string: text, 117 | attributes: [NSLinkAttributeName: URL]) 118 | 119 | let item = NSPasteboardItem() 120 | item.setData( 121 | hyperlink.RTFFromRange( 122 | NSMakeRange(0, hyperlink.length), 123 | documentAttributes: nil ), 124 | forType: NSPasteboardTypeRTF) 125 | item.setString( 126 | URL.absoluteString, 127 | forType: NSPasteboardTypeString) 128 | 129 | return item 130 | } 131 | -------------------------------------------------------------------------------- /Sukhasana/ViewModel/SettingsScreenModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsScreenModel.swift 3 | // Sukhasana 4 | // 5 | // Created by Tom Brow on 2/16/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import ReactiveCocoa 10 | 11 | struct SettingsScreenModel { 12 | let shortcutViewAssociatedUserDefaultsKey: String 13 | let APIKeyTextFieldText: MutableProperty 14 | let saveButtonEnabled, workspacePopUpButtonEnabled, progressIndicatorAnimating: PropertyOf 15 | let workspacePopUpItemsTitles: PropertyOf<[String]> 16 | let workspacePopupSelectedIndex: PropertyOf 17 | let didClickSaveButton: Signal<(), NoError>.Observer 18 | let workspacePopUpDidSelectItemAtIndex: Signal.Observer 19 | 20 | static func makeWithSettings(settings: Settings?, globalShortcutDefaultsKey: String) -> (SettingsScreenModel, didSaveSettings: SignalProducer) { 21 | let (didSaveSettings, didSaveSettingsSink) = SignalProducer.buffer(1) 22 | 23 | return ( 24 | SettingsScreenModel( 25 | settings: settings, 26 | globalShortcutDefaultsKey: globalShortcutDefaultsKey, 27 | didSaveSettingsSink: didSaveSettingsSink), 28 | didSaveSettings: didSaveSettings) 29 | } 30 | 31 | // MARK: private 32 | 33 | private init( 34 | settings: Settings?, 35 | globalShortcutDefaultsKey: String, 36 | didSaveSettingsSink: Signal.Observer) 37 | { 38 | shortcutViewAssociatedUserDefaultsKey = globalShortcutDefaultsKey 39 | APIKeyTextFieldText = MutableProperty(settings?.APIKey ?? "") 40 | 41 | let workspacesState: SignalProducer = APIKeyTextFieldText.producer 42 | |> map { APIClient(APIKey: $0) } 43 | |> map { $0.requestWorkspaces() } 44 | |> map { $0 |> map(workspacesFromJSON) } 45 | |> map { $0 46 | |> map { .Fetched($0) } 47 | |> catchTo(.Failed) 48 | |> startWith(.Fetching) 49 | } 50 | |> join(.Latest) 51 | |> replay(capacity: 1) 52 | 53 | workspacePopUpButtonEnabled = propertyOf(false, workspacesState 54 | |> map { switch $0 { 55 | case .Fetched(let workspaces): 56 | return !isEmpty(workspaces) 57 | case .Initial, .Fetching, .Failed: 58 | return false 59 | } 60 | }) 61 | 62 | saveButtonEnabled = workspacePopUpButtonEnabled 63 | 64 | progressIndicatorAnimating = propertyOf(false, workspacesState 65 | |> map { switch $0 { 66 | case .Fetching: 67 | return true 68 | case .Initial, .Fetched, .Failed: 69 | return false 70 | } 71 | }) 72 | 73 | workspacePopUpItemsTitles = propertyOf([], workspacesState 74 | |> map { switch $0 { 75 | case .Fetched(let workspaces): 76 | return isEmpty(workspaces) 77 | ? ["(no workspaces)"] 78 | : workspaces.map { $0.name } 79 | case .Initial, .Fetching, .Failed: 80 | // Placeholder text for when popup is disabled 81 | return ["Workspace"] 82 | } 83 | }) 84 | 85 | let (workspaceSelectedIndexes, workspaceSelectedIndexSink) = SignalProducer.buffer(1) 86 | workspacePopUpDidSelectItemAtIndex = workspaceSelectedIndexSink 87 | 88 | workspacePopupSelectedIndex = propertyOf(0, 89 | SignalProducer(values: [ 90 | workspaceSelectedIndexes, 91 | workspacePopUpItemsTitles.producer |> map { _ in 0 } 92 | ]) 93 | |> join(.Merge) 94 | ) 95 | 96 | let (didClickSaveButtonProducer, didClickSaveButtonSink) = SignalProducer<(), NoError>.buffer(1) 97 | didClickSaveButton = didClickSaveButtonSink 98 | combineLatest(workspacesState, workspacePopupSelectedIndex.producer, APIKeyTextFieldText.producer) 99 | |> sampleOn(didClickSaveButtonProducer) 100 | |> map { workspacesState, workspacePopupSelectedIndex, APIKeyTextFieldText in 101 | switch workspacesState { 102 | case .Fetched(let workspaces): 103 | return Settings( 104 | APIKey: APIKeyTextFieldText, 105 | workspaceID: workspaces[workspacePopupSelectedIndex].id) 106 | default: 107 | fatalError("can't save with no workspaces loaded") 108 | }} 109 | |> start(didSaveSettingsSink) 110 | 111 | } 112 | } 113 | 114 | private struct Workspace { 115 | let id, name: String 116 | } 117 | 118 | private enum WorkspacesState { 119 | case Initial 120 | case Fetching 121 | case Failed 122 | case Fetched([Workspace]) 123 | } 124 | 125 | private func workspacesFromJSON(JSON: NSDictionary) -> [Workspace] { 126 | var workspaces = [Workspace]() 127 | if let data = JSON["data"] as? Array> { 128 | for object in data { 129 | if let name = object["name"] as? String { 130 | if let id = object["id"] as? NSNumber { 131 | workspaces.append(Workspace(id: id.stringValue, name: name)) 132 | } 133 | } 134 | } 135 | } 136 | return workspaces 137 | } 138 | -------------------------------------------------------------------------------- /SukhasanaTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.tombrow.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SukhasanaTests/SukhasanaTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SukhasanaTests.swift 3 | // SukhasanaTests 4 | // 5 | // Created by Tom Brow on 2/4/15. 6 | // Copyright (c) 2015 Tom Brow. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import XCTest 11 | 12 | class SukhasanaTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | --------------------------------------------------------------------------------