├── .gitignore ├── GitHub ├── openurl_example.png ├── scr.png ├── scr.pxm ├── scr_retro.png ├── slash_logo.pxm ├── slash_logo_small.png └── slash_logo_small.pxm ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Application.swift ├── ChannelsListView.swift ├── CrashReporter.swift ├── MessagesListView.swift ├── R.swift ├── Server.swift ├── SlackAdapter.swift ├── SlackChannel.swift ├── SlackContext.swift ├── SlackEmojiDecoder.swift ├── SlackEvent.swift ├── SlackGroup.swift ├── SlackIM.swift ├── SlackMessage.swift ├── SlackMessageReaction.swift ├── SlackOAuth2.swift ├── SlackRealTimeClient.swift ├── SlackTeam.swift ├── SlackUser.swift ├── SlackWebClient.swift ├── Socket.swift ├── TLSSocket.swift ├── TerminalCanvas.swift ├── TerminalDevice.swift ├── TextLayout.swift ├── URLSession.swift ├── UserInputView.swift ├── Utils.swift ├── WebSocketClient.swift └── main.swift ├── slash.rb ├── slash.sublime-project └── slash.xcodeproj ├── project.pbxproj ├── project.xcworkspace └── contents.xcworkspacedata └── xcshareddata └── xcschemes └── slash.xcscheme /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | 5 | ## Various settings 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | 16 | ## Other 17 | *.moved-aside 18 | *.xcuserstate 19 | 20 | ## Obj-C/Swift specific 21 | *.hmap 22 | *.ipa 23 | *.dSYM.zip 24 | *.dSYM 25 | 26 | ## Playgrounds 27 | timeline.xctimeline 28 | playground.xcworkspace 29 | 30 | # Swift Package Manager 31 | # 32 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 33 | # Packages/ 34 | .build/ 35 | -------------------------------------------------------------------------------- /GitHub/openurl_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slash-hq/slash/01e55ce89b9015c98bae32ba1371ef7a48a3c0f0/GitHub/openurl_example.png -------------------------------------------------------------------------------- /GitHub/scr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slash-hq/slash/01e55ce89b9015c98bae32ba1371ef7a48a3c0f0/GitHub/scr.png -------------------------------------------------------------------------------- /GitHub/scr.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slash-hq/slash/01e55ce89b9015c98bae32ba1371ef7a48a3c0f0/GitHub/scr.pxm -------------------------------------------------------------------------------- /GitHub/scr_retro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slash-hq/slash/01e55ce89b9015c98bae32ba1371ef7a48a3c0f0/GitHub/scr_retro.png -------------------------------------------------------------------------------- /GitHub/slash_logo.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slash-hq/slash/01e55ce89b9015c98bae32ba1371ef7a48a3c0f0/GitHub/slash_logo.pxm -------------------------------------------------------------------------------- /GitHub/slash_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slash-hq/slash/01e55ce89b9015c98bae32ba1371ef7a48a3c0f0/GitHub/slash_logo_small.png -------------------------------------------------------------------------------- /GitHub/slash_logo_small.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slash-hq/slash/01e55ce89b9015c98bae32ba1371ef7a48a3c0f0/GitHub/slash_logo_small.pxm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "slash", 6 | products: [.executable(name: "slash", targets: ["slash"])], 7 | targets: [.target(name: "slash", path: "Sources")] 8 | ) 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logo 2 | 3 | Slack terminal client. 4 | 5 |
6 | 7 | ![screenshot](GitHub/scr.png) 8 | 9 | ## Getting started 10 | 11 | To use `slash` install it via: 12 | 13 | `brew install https://raw.githubusercontent.com/slash-hq/slash/master/slash.rb` 14 | 15 | and start it by running: 16 | 17 | ``` 18 | slash 19 | ``` 20 | 21 | Slash can be used with the [development token](https://api.slack.com/docs/oauth-test-tokens) too: 22 | 23 | ``` 24 | slash 25 | ``` 26 | 27 | ## Keys 28 | 29 | ``` 30 | Enter : Send a message 31 | Tab : Switch channel 32 | Up/Down: Scroll messages 33 | Ctrl-C : Quit 34 | ``` 35 | 36 | ## Commands 37 | 38 | ``` 39 | /openurl : open url with 40 | ``` 41 | 42 | logo 43 | 44 | ## Retro mode 45 | 46 | Thanks too [cool-retro-term](https://github.com/Swordfish90/cool-retro-term) project slash can run in retro mode 47 | 48 | ![screenshot](GitHub/scr_retro.png) 49 | 50 | ## Issues 51 | 52 | Please make sure to read the Issue Reporting Checklist before opening an issue. Issues not conforming to the guidelines may be closed immediately. 53 | 54 | ## How to contribute? 55 | 56 | 1. Fork this repository and clone to your Mac. 57 | 1. Open `slash.xcodeproj` in Xcode 58 | 1. Build 59 | 1. Open `Terminal` and run `/Build/Products/Debug/slash` 60 | 1. Make your changes, test and submit pull request! 61 | 62 | ## FAQ 63 | 64 | 1. Can I connect to multiple Slack teams? 65 | 66 | Yes, simply start multiple `slash` instances and login to a different team in each of them. 67 | 68 | ## License 69 | 70 | Apache License, Version 2.0 71 | -------------------------------------------------------------------------------- /Sources/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | class Application { 10 | 11 | private let terminalDevice : TerminalDevice 12 | private let messagesListView : MessagesListView 13 | private let userInputView : UserInputView 14 | private let channelsListView : ChannelsListView 15 | private let webClient : SlackWebClient 16 | private let context : SlackContext 17 | private let rtmClient : SlackRealTimeClient 18 | 19 | private var messages = Array() 20 | private var links = [String]() 21 | private var selectedChannel: String? = nil 22 | private var replyIdCounter = 0 23 | private var unreadChannelsIds = Set() 24 | private let adapter = SlackAdapter() 25 | 26 | init(usingDevice device: TerminalDevice, authenticatedBy token: String) throws { 27 | 28 | self.terminalDevice = device 29 | 30 | self.messagesListView = MessagesListView(self.terminalDevice) 31 | self.channelsListView = ChannelsListView(self.terminalDevice) 32 | self.userInputView = UserInputView(self.terminalDevice) 33 | 34 | self.webClient = SlackWebClient(authenticatedBy: token) 35 | 36 | self.terminalDevice.flush( 37 | TerminalCanvas() 38 | .hideCursor() 39 | .color(R.color.connectingTextColor) 40 | .cursor(1, 1) 41 | .text(R.string.connecting).buffer) 42 | 43 | let team = try self.webClient.rtm() 44 | 45 | self.rtmClient = try SlackRealTimeClient(team.wssUrl) 46 | self.context = SlackContext(withTeam: team) 47 | 48 | if let defaultChannel = self.context.defaultChannel { 49 | self.selectedChannel = defaultChannel 50 | let defaultChannelName = self.context.name(forId: defaultChannel) ?? "" 51 | self.userInputView.placeholder = String(format: R.string.inputPlaceholder, defaultChannelName) 52 | self.userInputView.draw() 53 | self.reloadChannel(defaultChannel, clearChat: false) 54 | } 55 | 56 | DispatchQueue.global(qos: .background).async { [unowned self] in 57 | while true { 58 | do { 59 | while true { 60 | if let event = try self.rtmClient.waitForEvent() { 61 | self.handleRealTimeSessionEvent(event) 62 | } 63 | } 64 | } catch { 65 | self.appendRow(MessagesListRow(spans: [TextSpan(String(format: R.string.connectionError, "\(error)"))])) 66 | } 67 | } 68 | } 69 | } 70 | 71 | func handleRealTimeSessionEvent(_ event: SlackEvent) { 72 | 73 | switch event { 74 | 75 | case .hello: 76 | 77 | self.appendRow(MessagesListRow(spans: [TextSpan(String(format: R.string.hello, self.context.teamName), withColor: R.color.helloTextColor)])) 78 | 79 | case .reply(let replyId, let ts): 80 | 81 | if let index = self.messages.index(where: { $0.id == "pending\(replyId)" }) { 82 | let old = self.messages.remove(at: index) 83 | var newSpans = [TextSpan]() 84 | newSpans.append(TextSpan(adapter.formatSlackTimestamp("\(Date().timeIntervalSince1970)") + " ", withColor: R.color.messageTimeTextColor)) 85 | newSpans.append(contentsOf: old.spans) 86 | self.messages.insert(MessagesListRow(channel: old.channel, id: ts, spans: newSpans), at: index) 87 | self.messagesListView.draw(self.messages) 88 | } 89 | 90 | case .message(let message): 91 | 92 | if let selectedChannel = self.selectedChannel { 93 | if message.channel == selectedChannel { 94 | self.appendRow(self.messageListRowFor(message: message)) 95 | } else { 96 | self.unreadChannelsIds.insert(message.channel) 97 | self.channelsListView.draw(self.context, selectionId: self.selectedChannel, unreadIds: self.unreadChannelsIds) 98 | } 99 | } 100 | 101 | case .messageChanged(let message): 102 | 103 | if let index = self.messages.index(where: { $0.id == message.ts && $0.channel == message.channel }) { 104 | self.messages[index] = self.messageListRowFor(message: message) 105 | self.messagesListView.draw(messages) 106 | } 107 | 108 | case .messageDeleted(let ts, let channel): 109 | 110 | if let index = self.messages.index(where: { $0.id == ts && $0.channel == channel }) { 111 | self.messages.remove(at: index) 112 | self.messagesListView.draw(self.messages) 113 | } 114 | 115 | case .presenceChange(let user, let presence): 116 | 117 | if let index = self.context.users.index(where: { $0.id == user }) { 118 | let old = self.context.users.remove(at: index) 119 | self.context.users.insert(SlackUser(id: old.id, name: old.name, color: old.color, presence: presence), at: index) 120 | self.channelsListView.draw(self.context, selectionId: self.selectedChannel, unreadIds: self.unreadChannelsIds) 121 | } 122 | 123 | case .teamRename(let newName): 124 | 125 | self.context.teamName = newName 126 | 127 | self.channelsListView.draw(self.context, selectionId: self.selectedChannel, unreadIds: self.unreadChannelsIds) 128 | 129 | case .desktopNotification(let title, let message): 130 | 131 | #if os(Linux) 132 | // Linux notifications not implemented yet 133 | #else 134 | Process.launchedProcess(launchPath: "/usr/bin/osascript", arguments: ["-e", "display notification \"\(message)\" with title \"slash: \(title)\""]) 135 | #endif 136 | return 137 | 138 | case .messageReactionAdded(let reaction, let channel, let ts): 139 | 140 | if channel == self.selectedChannel { 141 | 142 | self.messagesListView.draw(self.messages) 143 | } 144 | 145 | case .unknown(let message): 146 | 147 | return self.appendRow(MessagesListRow(spans: ["?", " : ", TextSpan(message)])) 148 | 149 | default: 150 | 151 | break 152 | } 153 | } 154 | 155 | func notifyTerminalSizeHasChanged() { 156 | self.messagesListView.draw(self.messages) 157 | self.channelsListView.draw(self.context, selectionId: self.selectedChannel) 158 | self.userInputView.draw() 159 | } 160 | 161 | private func messageListRowFor(message: SlackMessage) -> MessagesListRow { 162 | let spans = self.adapter.textSpansFor(message: message, withContext: self.context, andLinks: &self.links) 163 | return MessagesListRow(channel: message.channel, id: message.ts, spans: spans) 164 | } 165 | 166 | func appendRow(_ row: MessagesListRow) { 167 | self.messages.insert(row, at: 0) 168 | self.messagesListView.draw(messages) 169 | } 170 | 171 | func reloadChannel(_ channelId: String, clearChat: Bool = true) { 172 | let loadingRow = MessagesListRow(spans: [TextSpan(R.string.loading, withColor: R.color.loadingTextColor)]) 173 | if clearChat { 174 | self.messagesListView.draw([loadingRow]) 175 | } else { 176 | self.messages.append(loadingRow) 177 | self.messagesListView.draw(self.messages) 178 | } 179 | DispatchQueue.global(qos: .background).async { [unowned self] in 180 | do { 181 | let rows = try self.webClient.history(for: channelId).map { 182 | self.messageListRowFor(message: $0) 183 | } 184 | //TODO notify about the results in a common queue for all the background tasks. 185 | self.messages.removeAll(keepingCapacity: true) 186 | self.messages.append(contentsOf: rows) 187 | self.messagesListView.draw(self.messages) 188 | self.channelsListView.draw(self.context, selectionId: self.selectedChannel, unreadIds: self.unreadChannelsIds) 189 | self.userInputView.draw() 190 | } catch { 191 | //TODO handle error case. 192 | } 193 | } 194 | } 195 | 196 | func run() { 197 | 198 | self.messagesListView.draw(self.messages) 199 | 200 | while true { 201 | 202 | let key = self.terminalDevice.key() 203 | 204 | switch key { 205 | 206 | case .ctrlC: 207 | 208 | try? self.terminalDevice.reset() 209 | exit(1) 210 | 211 | case .enter: 212 | 213 | guard !self.userInputView.input.isEmpty, let targetChannel = self.selectedChannel, let me = self.context.me else { 214 | continue 215 | } 216 | 217 | let message = self.userInputView.input 218 | 219 | if !self.executeLocalCommand(message) { 220 | try? self.rtmClient.send(targetChannel, message: message, replyId: replyIdCounter) 221 | 222 | let pendingMessage = MessagesListRow(channel: targetChannel, id: "pending\(replyIdCounter)", spans: [ 223 | TextSpan(me.name, withColor: Utils.xterm256Color(forUser: me)), 224 | TextSpan(": ", withColor: R.color.messagePrefixTextColor), 225 | TextSpan(message, withColor: R.color.messageTextColor)], pending: true) 226 | 227 | self.messages.insert(pendingMessage, at: 0) 228 | 229 | self.replyIdCounter = self.replyIdCounter + 1 230 | 231 | self.messagesListView.draw(self.messages) 232 | } 233 | 234 | self.userInputView.input = "" 235 | self.userInputView.cursor = 0 236 | self.userInputView.draw() 237 | 238 | case .backspace: 239 | 240 | if self.userInputView.input.isEmpty || self.userInputView.cursor <= 0 { 241 | continue 242 | } 243 | self.userInputView.input.remove(at: 244 | userInputView.input.index(self.userInputView.input.startIndex, offsetBy: self.userInputView.cursor-1)) 245 | self.userInputView.cursor = max(0, self.userInputView.cursor - 1) 246 | self.userInputView.draw() 247 | 248 | case .arrowLeft: 249 | 250 | self.userInputView.cursor = max(0, userInputView.cursor - 1) 251 | self.userInputView.draw() 252 | 253 | case .arrowRight: 254 | 255 | self.userInputView.cursor = min(userInputView.input.count, userInputView.cursor + 1) 256 | self.userInputView.draw() 257 | 258 | case .arrowUp: 259 | 260 | self.messagesListView.scroll = self.messagesListView.scroll + 1 261 | self.messagesListView.draw(self.messages) 262 | 263 | case .arrowDown: 264 | 265 | guard self.messagesListView.scroll > 0 else { 266 | break 267 | } 268 | 269 | self.messagesListView.scroll = self.messagesListView.scroll - 1 270 | self.messagesListView.draw(self.messages) 271 | 272 | case .tab(let withShift): 273 | 274 | guard let suggestion = self.context.suggestRecipient(for: self.selectedChannel, unreadIds: self.unreadChannelsIds, backwardSearch: withShift) else { 275 | continue 276 | } 277 | 278 | self.userInputView.input = "" 279 | self.userInputView.cursor = 0 280 | self.messagesListView.scroll = 0 281 | 282 | self.unreadChannelsIds.remove(suggestion.id) 283 | 284 | self.selectedChannel = suggestion.id 285 | self.userInputView.placeholder = String(format: R.string.inputPlaceholder, suggestion.name) 286 | self.channelsListView.draw(self.context, selectionId: suggestion.id, unreadIds: self.unreadChannelsIds) 287 | 288 | self.reloadChannel(suggestion.id) 289 | self.links.removeAll() 290 | self.userInputView.draw() 291 | 292 | case .other(let character): 293 | 294 | self.userInputView.input.insert(character, 295 | at: self.userInputView.input.index(self.userInputView.input.startIndex, 296 | offsetBy: self.userInputView.cursor)) 297 | 298 | self.userInputView.cursor = userInputView.cursor + 1 299 | self.userInputView.draw() 300 | 301 | default: break 302 | 303 | } 304 | } 305 | } 306 | 307 | /// Execute a local "slash" command. 308 | /// 309 | /// The currently supported list of local commands are; 310 | /// 311 | /// - /openurl {space separated list of numbers}: opens numbered link in default browser 312 | /// 313 | func executeLocalCommand(_ command: String) -> Bool { 314 | if (command.hasPrefix("/openurl")) { 315 | let pieces = command.components(separatedBy: [",", " "]) 316 | let linkNumbers = pieces.compactMap { Int($0) } 317 | for linkNumber in linkNumbers { 318 | let url = self.links[linkNumber - 1] 319 | Utils.shell("open", url) 320 | } 321 | return true 322 | } 323 | 324 | return false 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /Sources/ChannelsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | 8 | import Foundation 9 | 10 | struct ChannelListRow { 11 | 12 | let id: String 13 | let name: String 14 | } 15 | 16 | class ChannelsListView { 17 | 18 | private let canvas = TerminalCanvas() 19 | 20 | private let terminalDevice: TerminalDevice 21 | 22 | init(_ device: TerminalDevice) { 23 | self.terminalDevice = device 24 | } 25 | 26 | func drawRow(_ text: String, row: Int, highlight: Bool = false, blink: Bool = false) { 27 | 28 | let availableWidth = R.dimen.channelsListWidth 29 | 30 | let paddedName = text.count > availableWidth ? String(text[.. = []) { 41 | 42 | let size = self.terminalDevice.size 43 | 44 | let avaibleHeight = size.height 45 | 46 | guard size.width > 0 && avaibleHeight > 0 else { return } 47 | 48 | var offset = 1 49 | 50 | self.canvas 51 | .clear() 52 | .hideCursor() 53 | 54 | // Draw team name. 55 | 56 | self.canvas 57 | .color(R.color.teamNameTextColor) 58 | .background(R.color.teamNameBackgroundColor) 59 | .cursor(1, 1) 60 | .text(context.teamName.padding(toLength: R.dimen.channelsListWidth, withPad: " ", startingAt: 0)) 61 | 62 | offset = offset + 1 63 | 64 | // Draw channels. 65 | 66 | self.canvas.color(R.color.channelNameTextColor); 67 | 68 | for channel in context.channels { 69 | if (!channel.isMember) { 70 | continue 71 | } 72 | 73 | self.drawRow("#" + channel.name, row: offset, highlight: channel.id == selectionId, blink: unreadIds.contains(channel.id)) 74 | 75 | offset = offset + 1 76 | if offset > avaibleHeight { 77 | break 78 | } 79 | } 80 | 81 | // Draw groups. 82 | 83 | self.canvas.color(R.color.groupNameTextColor); 84 | 85 | if offset < avaibleHeight { 86 | for group in context.groups { 87 | 88 | self.drawRow("#" + group.name, row: offset, highlight: group.id == selectionId, blink: unreadIds.contains(group.id)) 89 | 90 | offset = offset + 1 91 | if offset > avaibleHeight { 92 | break 93 | } 94 | } 95 | } 96 | 97 | // Draw members. 98 | 99 | if offset < avaibleHeight { 100 | for im in context.ims { 101 | let slackUser = context.user(forId: im.user) ?? SlackUser(id: "", name: "", color: "", presence: .away) 102 | if slackUser.presence == .active { 103 | self.canvas.color(Utils.xterm256Color(forUser: slackUser)) 104 | } else { 105 | self.canvas.color(R.color.channelListTextColorAway) 106 | } 107 | self.drawRow("@" + (context.user(forId: im.user)?.name ?? ""), row: offset, highlight: im.id == selectionId, blink: unreadIds.contains(im.id)) 108 | offset = offset + 1 109 | if offset > avaibleHeight { 110 | break 111 | } 112 | } 113 | } 114 | 115 | self.canvas 116 | .color(R.color.channelNameTextColor) 117 | .background(R.color.channelListBgColor) 118 | 119 | while offset <= avaibleHeight { 120 | self.canvas 121 | .cursor(1, offset) 122 | .text(String(repeating: " ", count: R.dimen.channelsListWidth)) 123 | offset = offset + 1 124 | } 125 | 126 | self.canvas.reset() 127 | 128 | terminalDevice.flush(self.canvas.buffer) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/CrashReporter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | 10 | struct CrashReporter { 11 | 12 | private static var terminalDevice: TerminalDevice? = nil 13 | 14 | static func watch(usingDevice device: TerminalDevice) { 15 | 16 | CrashReporter.terminalDevice = device 17 | 18 | NSSetUncaughtExceptionHandler { _ in 19 | CrashReporter.report() 20 | } 21 | 22 | signal(SIGABRT) { _ in 23 | CrashReporter.report() 24 | } 25 | 26 | signal(SIGILL) { _ in 27 | CrashReporter.report() 28 | } 29 | 30 | signal(SIGSEGV) { _ in 31 | CrashReporter.report() 32 | } 33 | 34 | signal(SIGFPE) { _ in 35 | CrashReporter.report() 36 | } 37 | 38 | signal(SIGBUS) { _ in 39 | CrashReporter.report() 40 | } 41 | 42 | signal(SIGPIPE) { _ in 43 | CrashReporter.report() 44 | } 45 | } 46 | 47 | private static func report() { 48 | 49 | try? CrashReporter.terminalDevice?.reset() 50 | 51 | let stacktrace = Thread.callStackSymbols.joined(separator: "\n") 52 | 53 | let title = "Crash detected".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" 54 | 55 | let body = ("I found the following crash in the app:\n```" + stacktrace + "```") 56 | .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" 57 | 58 | CrashReporter.terminalDevice?.flush(TerminalCanvas() 59 | .reset() 60 | .clear() 61 | .text("Oooops.... we are really sorry but slash has crashed ☠️ :\n\nSTACKTRACE:\n\n") 62 | .text(stacktrace) 63 | .text("\n\n") 64 | .text("Please copy this very long link to the browser, to help us with fixing the crash 🙇 \n\nhttps://github.com/slash-hq/slash/issues/new?title=\(title)&body=\(body))\n\n") 65 | .text("Thank you, \nslash Team\n\n") 66 | .buffer) 67 | 68 | exit(1) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/MessagesListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | struct MessagesListRow { 10 | 11 | let channel: String? 12 | let id: String? 13 | let spans: [TextSpan] 14 | let pending: Bool 15 | 16 | init(channel: String? = nil, id: String? = nil, spans: [TextSpan], pending: Bool = false) { 17 | self.channel = channel 18 | self.id = id 19 | self.spans = spans 20 | self.pending = pending 21 | } 22 | } 23 | 24 | class MessagesListView { 25 | 26 | private let canvas = TerminalCanvas() 27 | private let textLayout = TextLayout() 28 | 29 | private let terminalDevice: TerminalDevice 30 | 31 | init(_ device: TerminalDevice) { 32 | self.terminalDevice = device 33 | } 34 | 35 | var scroll = 0 36 | 37 | func draw(_ rows: Array) { 38 | 39 | let size = self.terminalDevice.size 40 | let bottomPadding = 2 41 | 42 | guard size.width > 0 && size.height > bottomPadding else { 43 | return 44 | } 45 | 46 | let avaibleHeight = size.height - bottomPadding 47 | 48 | self.canvas 49 | .clear() 50 | .hideCursor() 51 | 52 | let column = R.dimen.channelsListWidth + 2 53 | 54 | var rayOffset = avaibleHeight 55 | var scrollOffset = self.scroll 56 | 57 | // Draw from the bottom to the top. 58 | 59 | lines: for row in rows { 60 | 61 | let lines = self.textLayout.layout(row.spans, alignToWidth: size.width - column - 1) 62 | 63 | for line in lines.reversed() { 64 | 65 | if scrollOffset > 0 { 66 | scrollOffset = scrollOffset - 1 67 | continue 68 | } 69 | 70 | self.canvas 71 | .cursor(column-1, rayOffset) 72 | .background(R.color.messagesListBgColor) 73 | .text(" ") 74 | .buffer.append(contentsOf: line) 75 | 76 | rayOffset = rayOffset - 1 77 | if rayOffset < 1 { 78 | break lines 79 | } 80 | } 81 | } 82 | 83 | // Clear the top if needed. 84 | 85 | if rayOffset > 0 { 86 | for i in 1...rayOffset { 87 | self.canvas 88 | .cursor(R.dimen.channelsListWidth + 1, i) 89 | .color(R.color.defaulTextColor) 90 | .background(R.color.messagesListBgColor) 91 | .text(String(repeating: " ", count: size.width - R.dimen.channelsListWidth)) 92 | } 93 | } 94 | 95 | self.canvas.reset() 96 | 97 | terminalDevice.flush(self.canvas.buffer) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/R.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | class R { 10 | 11 | class string { 12 | 13 | // Cool emojis: 🚫❕❗️📢📣🚀🐌🌊💦🌪❄️💥💡🗑⏱🔪💉🌡🚿🎈🎁🏷🔖🔐🔒🔓🔏⚠️💩 14 | 15 | static let inputPlaceholder = "Message #%@..." 16 | 17 | static let unknownMessageAuthor = "(unknown)" 18 | 19 | static let directMessageAuthor = "private" 20 | 21 | static let hello = "Connected to %@ team." 22 | 23 | static let unknownMessageChannel = "??" 24 | 25 | static let connecting = "Connecting 💤 ..." 26 | 27 | static let loading = "Loading 💤 ..." 28 | 29 | static let connectionError = "⚠️ Connection error occured (%@)" 30 | 31 | static let me = "me" 32 | 33 | static let authHelpMessage = "🔒 Visit ( %@ ) to login using OAuth2..." 34 | 35 | static let authConfirmation = 36 | "
" + 37 | "


" + 38 | "

You can close this window now and continue in the terminal.


" + 39 | "" + 40 | "
" 41 | } 42 | 43 | class color { 44 | 45 | // XTERM-256 palette: https://jonasjacek.github.io/colors/ 46 | 47 | static let defaultBgColor = -1 48 | 49 | static let defaulTextColor = -1 50 | 51 | static let connectingTextColor = 255 52 | 53 | static let helloTextColor = 255 54 | 55 | static let commandTextColor = 201 56 | 57 | static let userInputTextColor = 231 58 | 59 | static let userInputBackgorundColor = 16 60 | 61 | static let userInputPlaceholderTextColor = 242 62 | 63 | static let userInputSeparatorTextColor = 255 64 | 65 | static let messageAuthorTextColor = 158 66 | 67 | static let messagePrefixTextColor = 104 68 | 69 | static let messageTextColor = 252 70 | 71 | static let messageTimeTextColor = 24 72 | 73 | static let messageHighlightedBgColor = 154 74 | 75 | static let teamNameTextColor = 15 76 | 77 | static let teamNameBackgroundColor = 234 78 | 79 | static let channelNameTextColor = 38 80 | 81 | static let groupNameTextColor = 190 82 | 83 | static let directMessageTextColor = 240 84 | 85 | static let channelListTextColorAway = 239 86 | 87 | static let channelListBgColor = 232 88 | 89 | static let channelListBgColorSelected = 15 90 | 91 | static let channelListBgColorNotRead = 148 92 | 93 | static let messagesListBgColor = 0 94 | 95 | static let loadingTextColor = 11 96 | 97 | static let linkTextColor = 11 98 | 99 | static let mentionTextColor = 37 100 | 101 | static let reactionTextColor = 15 102 | } 103 | 104 | class dimen { 105 | 106 | static let channelsListWidth = 17 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Server.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | class Request { 10 | 11 | enum HttpVersion { case http10, http11 } 12 | 13 | var httpVersion = HttpVersion.http10 14 | 15 | var method = "" 16 | 17 | var path = "" 18 | 19 | var query = [(String, String)]() 20 | 21 | var headers = [(String, String)]() 22 | 23 | var body = [UInt8]() 24 | 25 | var contentLength = 0 26 | 27 | func hasToken(_ token: String, forHeader headerName: String) -> Bool { 28 | guard let (_, value) = headers.filter({ $0.0 == headerName }).first else { 29 | return false 30 | } 31 | return value 32 | .components(separatedBy: ",") 33 | .filter({ $0.trimmingCharacters(in: .whitespaces).lowercased() == token }) 34 | .count > 0 35 | } 36 | } 37 | 38 | class Response { 39 | 40 | init() { } 41 | 42 | init(_ status: Status = Status.ok) { 43 | self.status = status.rawValue 44 | } 45 | 46 | init(_ status: Int = Status.ok.rawValue) { 47 | self.status = status 48 | } 49 | 50 | init(_ body: Array) { 51 | self.body.append(contentsOf: body) 52 | } 53 | 54 | init(_ body: ArraySlice) { 55 | self.body.append(contentsOf: body) 56 | } 57 | 58 | var status = Status.ok.rawValue 59 | 60 | var headers = [(String, String)]() 61 | 62 | var body = [UInt8]() 63 | 64 | var processingSuccesor: IncomingDataProcessor? = nil 65 | } 66 | 67 | class TextResponse: Response { 68 | 69 | init(_ status: Int = Status.ok.rawValue, _ text: String) { 70 | super.init(status) 71 | self.headers.append(("Content-Type", "text/plain")) 72 | self.body = [UInt8](text.utf8) 73 | } 74 | } 75 | 76 | class HtmlResponse: Response { 77 | 78 | init(_ status: Int = Status.ok.rawValue, _ text: String) { 79 | super.init(status) 80 | self.headers.append(("Content-Type", "text/html")) 81 | self.body = [UInt8](text.utf8) 82 | } 83 | } 84 | 85 | enum Status: Int { 86 | case `continue` = 100 87 | case switchingProtocols = 101 88 | case ok = 200 89 | case created = 201 90 | case accepted = 202 91 | case noContent = 204 92 | case resetContent = 205 93 | case partialContent = 206 94 | case movedPerm = 301 95 | case notModified = 304 96 | case badRequest = 400 97 | case unauthorized = 401 98 | case forbidden = 403 99 | case notFound = 404 100 | } 101 | 102 | extension UInt8 { 103 | 104 | static var 105 | lf: UInt8 = 10, 106 | cr: UInt8 = 13, 107 | space: UInt8 = 32, 108 | colon: UInt8 = 58, 109 | ampersand: UInt8 = 38, 110 | lessThan: UInt8 = 60, 111 | greaterThan: UInt8 = 62, 112 | slash: UInt8 = 47, 113 | equal: UInt8 = 61, 114 | doubleQuotes: UInt8 = 34, 115 | openingParenthesis: UInt8 = 40, 116 | closingParenthesis: UInt8 = 41, 117 | comma: UInt8 = 44 118 | } 119 | 120 | enum AsyncError: Error { 121 | case parse(String) 122 | case async(String) 123 | case socketCreation(String) 124 | case setReUseAddr(String) 125 | case setNoSigPipeFailed(String) 126 | case setNonBlockFailed(String) 127 | case setReuseAddrFailed(String) 128 | case bindFailed(String) 129 | case listenFailed(String) 130 | case writeFailed(String) 131 | case getPeerNameFailed(String) 132 | case convertingPeerNameFailed 133 | case getNameInfoFailed(String) 134 | case acceptFailed(String) 135 | case readFailed(String) 136 | case httpError(String) 137 | } 138 | 139 | protocol TcpServer { 140 | 141 | init(_ port: in_port_t) throws 142 | 143 | func wait(_ callback: ((TcpServerEvent) -> Void)) throws 144 | 145 | func write(_ socket: Int32, _ data: Array, _ done: @escaping (() -> TcpWriteDoneAction)) throws 146 | 147 | func finish(_ socket: Int32) 148 | } 149 | 150 | enum TcpWriteDoneAction { 151 | 152 | case `continue` 153 | 154 | case terminate 155 | } 156 | 157 | enum TcpServerEvent { 158 | 159 | case connect(String, Int32) 160 | 161 | case disconnect(String, Int32) 162 | 163 | case data(String, Int32, ArraySlice) 164 | } 165 | 166 | class HttpIncomingDataPorcessor: Hashable, IncomingDataProcessor { 167 | 168 | private enum State { 169 | case waitingForHeaders 170 | case waitingForBody 171 | } 172 | 173 | private var state = State.waitingForHeaders 174 | 175 | private let socket: Int32 176 | private var buffer = Array() 177 | private var request = Request() 178 | private let callback: ((Request) throws -> Void) 179 | 180 | init(_ socket: Int32, _ closure: @escaping ((Request) throws -> Void)) { 181 | self.socket = socket 182 | self.callback = closure 183 | } 184 | 185 | static func == (lhs: HttpIncomingDataPorcessor, rhs: HttpIncomingDataPorcessor) -> Bool { 186 | return lhs.socket == rhs.socket 187 | } 188 | 189 | var hashValue: Int { return Int(self.socket) } 190 | 191 | func process(_ chunk: ArraySlice) throws { 192 | 193 | switch self.state { 194 | 195 | case .waitingForHeaders: 196 | 197 | guard self.buffer.count + chunk.count < 4096 else { 198 | throw AsyncError.parse("Headers size exceeds that limit.") 199 | } 200 | 201 | var iterator = chunk.makeIterator() 202 | 203 | while let byte = iterator.next() { 204 | if byte != UInt8.cr { 205 | buffer.append(byte) 206 | } 207 | if buffer.count >= 2 && buffer[buffer.count-1] == UInt8.lf && buffer[buffer.count-2] == UInt8.lf { 208 | self.buffer.removeLast(2) 209 | self.request = try self.consumeHeader(buffer) 210 | self.buffer.removeAll(keepingCapacity: true) 211 | let left = [UInt8](iterator) 212 | self.state = .waitingForBody 213 | try self.process(left[0.. Request { 234 | 235 | let lines = data.split(separator: UInt8.lf) 236 | 237 | guard let requestLine = lines.first else { 238 | throw AsyncError.httpError("No status line.") 239 | } 240 | 241 | let requestLineTokens = requestLine.split(separator: UInt8.space) 242 | 243 | guard requestLineTokens.count >= 3 else { 244 | throw AsyncError.httpError("Invalid status line.") 245 | } 246 | 247 | let request = Request() 248 | 249 | if requestLineTokens[2] == [0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x30] { 250 | request.httpVersion = .http10 251 | } else if requestLineTokens[2] == [0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31] { 252 | request.httpVersion = .http11 253 | } else { 254 | throw AsyncError.parse("Invalid http version: \(requestLineTokens[2])") 255 | } 256 | 257 | request.headers = lines 258 | .dropFirst() 259 | .map { line in 260 | let headerTokens = line.split(separator: UInt8.colon, maxSplits: 1) 261 | if let name = headerTokens.first, let value = headerTokens.last { 262 | if let nameString = String(bytes: name, encoding: .ascii), 263 | let valueString = String(bytes: value, encoding: .ascii) { 264 | return (nameString.lowercased(), valueString.trimmingCharacters(in: .whitespaces)) 265 | } 266 | } 267 | return ("", "") 268 | } 269 | 270 | if let (_, value) = request.headers 271 | .filter({ $0.0 == "content-length" }) 272 | .first { 273 | guard let contentLength = Int(value) else { 274 | throw AsyncError.parse("Invalid 'Content-Length' header value \(value).") 275 | } 276 | request.contentLength = contentLength 277 | } 278 | 279 | guard let method = String(bytes: requestLineTokens[0], encoding: .ascii) else { 280 | throw AsyncError.parse("Invalid 'method' value \(requestLineTokens[0]).") 281 | } 282 | 283 | request.method = method 284 | 285 | guard let path = String(bytes: requestLineTokens[1], encoding: .ascii) else { 286 | throw AsyncError.parse("Invalid 'path' value \(requestLineTokens[1]).") 287 | } 288 | 289 | let queryComponents = path.components(separatedBy: "?") 290 | 291 | if queryComponents.count > 1, let first = queryComponents.first, let last = queryComponents.last { 292 | request.path = first 293 | request.query = last 294 | .components(separatedBy: "&") 295 | .reduce([(String, String)]()) { (c, s) -> [(String, String)] in 296 | let tokens = s.components(separatedBy: "=") 297 | if let name = tokens.first, let value = tokens.last { 298 | if let nameDecoded = name.removingPercentEncoding, let valueDecoded = value.removingPercentEncoding { 299 | return c + [(nameDecoded, tokens.count > 1 ? valueDecoded : "")] 300 | } 301 | } 302 | return c 303 | } 304 | } else { 305 | request.path = path 306 | } 307 | 308 | return request 309 | } 310 | } 311 | 312 | class Server { 313 | 314 | private var processors = [Int32 : IncomingDataProcessor]() 315 | 316 | private let server: TcpServer 317 | 318 | init(_ port: in_port_t = 8080) throws { 319 | #if os(Linux) 320 | self.server = try LinuxAsyncServer(port) 321 | #else 322 | self.server = try MacOSAsyncTCPServer(port) 323 | #endif 324 | } 325 | 326 | func serve(_ callback: @escaping ((request: Request, responder: ((Response) -> Void))) -> Void) throws { 327 | 328 | try self.server.wait { event in 329 | 330 | switch event { 331 | 332 | case .connect(_, let socket): 333 | 334 | self.processors[socket] = HttpIncomingDataPorcessor(socket) { request in 335 | callback((request, { response in 336 | let keepIOSession = self.supportsKeepAlive(request.headers) || request.httpVersion == .http11 337 | var data = [UInt8]() 338 | data.reserveCapacity(1024) 339 | data.append(contentsOf: [UInt8]("HTTP/\(request.httpVersion == .http10 ? "1.0" : "1.1") \(response.status) OK\r\n".utf8)) 340 | for (name, value) in response.headers { 341 | data.append(contentsOf: [UInt8]("\(name): \(value)\r\n".utf8)) 342 | } 343 | if (keepIOSession) { 344 | data.append(contentsOf: [UInt8]("Connection: keep-alive\r\n".utf8)) 345 | } 346 | data.append(contentsOf: [UInt8]("Content-Length: \(response.body.count)\r\n".utf8)) 347 | data.append(contentsOf: [13, 10]) 348 | data.append(contentsOf: response.body) 349 | do { 350 | try self.server.write(socket, data) { 351 | if let sucessor = response.processingSuccesor { 352 | self.processors[socket] = sucessor 353 | return .continue 354 | } 355 | return keepIOSession ? .continue : .terminate 356 | } 357 | } catch { 358 | self.processors.removeValue(forKey: socket) 359 | } 360 | })) 361 | } 362 | 363 | case .disconnect(_, let socket): 364 | 365 | self.processors.removeValue(forKey: socket) 366 | 367 | case .data(_, let socket, let chunk): 368 | 369 | do { 370 | try self.processors[socket]?.process(chunk) 371 | } catch { 372 | self.processors.removeValue(forKey: socket) 373 | self.server.finish(socket) 374 | } 375 | } 376 | } 377 | } 378 | 379 | private func supportsKeepAlive(_ headers: Array<(String, String)>) -> Bool { 380 | if let (_, value) = headers.filter({ $0.0 == "connection" }).first { 381 | return "keep-alive" == value.trimmingCharacters(in: CharacterSet.whitespaces) 382 | } 383 | return false 384 | } 385 | 386 | private func closeConnection(_ headers: Array<(String, String)>) -> Bool { 387 | if let (_, value) = headers.filter({ $0.0 == "connection" }).first { 388 | return "close" == value.trimmingCharacters(in: CharacterSet.whitespaces) 389 | } 390 | return false 391 | } 392 | } 393 | 394 | protocol IncomingDataProcessor { 395 | 396 | func process(_ chunk: ArraySlice) throws 397 | } 398 | 399 | extension Process { 400 | 401 | static var pid: Int { 402 | return Int(getpid()) 403 | } 404 | 405 | static var tid: UInt64 { 406 | #if os(Linux) 407 | return UInt64(pthread_self()) 408 | #else 409 | var tid: __uint64_t = 0 410 | pthread_threadid_np(nil, &tid); 411 | return UInt64(tid) 412 | #endif 413 | } 414 | 415 | static var error: String { 416 | return String(cString: UnsafePointer(strerror(errno))) 417 | } 418 | } 419 | 420 | class MacOSAsyncTCPServer: TcpServer { 421 | 422 | private var backlog = Dictionary TcpWriteDoneAction))>>() 423 | private var peers = Set() 424 | 425 | private let kernelQueue: KernelQueue 426 | private let server: UInt 427 | 428 | required init(_ port: in_port_t = 8080) throws { 429 | 430 | self.kernelQueue = try KernelQueue() 431 | 432 | self.server = UInt(try MacOSAsyncTCPServer.nonBlockingSocketForListenening(port)) 433 | 434 | self.kernelQueue.subscribe(server, .read) 435 | } 436 | 437 | func write(_ socket: Int32, _ data: Array, _ done: @escaping (() -> TcpWriteDoneAction)) throws { 438 | 439 | let result = Socket.write(socket, data, data.count) 440 | 441 | if result == -1 { 442 | defer { self.finish(socket) } 443 | throw AsyncError.writeFailed(Process.error) 444 | } 445 | 446 | if result == data.count { 447 | if done() == .terminate { 448 | self.finish(socket) 449 | } 450 | return 451 | } 452 | 453 | self.backlog[socket]?.append(([UInt8](data[result.. Void)) throws { 458 | try self.kernelQueue.wait { signal in 459 | switch signal.event { 460 | case .read: 461 | if signal.ident == self.server { 462 | let client = try MacOSAsyncTCPServer.acceptAndConfigureClientSocket(Int32(signal.ident)) 463 | self.peers.insert(client) 464 | self.backlog[Int32(client)] = [] 465 | kernelQueue.subscribe(UInt(client), .read) 466 | kernelQueue.subscribe(UInt(client), .write) 467 | kernelQueue.pause(UInt(client), .write) 468 | callback(.connect("", Int32(client))) 469 | } else { 470 | var chunk = [UInt8](repeating: 0, count: signal.data) 471 | let result = Socket.read(Int32(signal.ident), &chunk, signal.data) 472 | if result <= 0 { 473 | finish(Int32(signal.ident)) 474 | callback(.disconnect("", Int32(signal.ident))) 475 | } else { 476 | callback(.data("", Int32(signal.ident), chunk[0.. Int32 { 532 | 533 | let server = Darwin.socket(AF_INET, SOCK_STREAM, 0) 534 | 535 | guard server != -1 else { 536 | throw AsyncError.socketCreation(Process.error) 537 | } 538 | 539 | var value: Int32 = 1 540 | if Darwin.setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout.size)) == -1 { 541 | defer { let _ = Socket.close(server) } 542 | throw AsyncError.setReuseAddrFailed(Process.error) 543 | } 544 | 545 | try setSocketNonBlocking(server) 546 | try setSocketNoSigPipe(server) 547 | 548 | var addr = anyAddrForPort(port) 549 | 550 | if withUnsafePointer(to: &addr, { Darwin.bind(server, UnsafePointer(OpaquePointer($0)), socklen_t(MemoryLayout.size)) }) == -1 { 551 | defer { let _ = Socket.close(server) } 552 | throw AsyncError.bindFailed(Process.error) 553 | } 554 | 555 | if Darwin.listen(server, SOMAXCONN) == -1 { 556 | defer { let _ = Socket.close(server) } 557 | throw AsyncError.listenFailed(Process.error) 558 | } 559 | 560 | return server 561 | } 562 | 563 | static func acceptAndConfigureClientSocket(_ socket: Int32) throws -> Int32 { 564 | 565 | guard case let client = Darwin.accept(socket, nil, nil), client != -1 else { 566 | throw AsyncError.acceptFailed(Process.error) 567 | } 568 | 569 | try self.setSocketNonBlocking(client) 570 | try self.setSocketNoSigPipe(client) 571 | 572 | return client 573 | } 574 | 575 | static func anyAddrForPort(_ port: in_port_t) -> sockaddr_in { 576 | var addr = sockaddr_in() 577 | addr.sin_len = __uint8_t(MemoryLayout.size) 578 | addr.sin_family = sa_family_t(AF_INET) 579 | addr.sin_port = port.bigEndian 580 | addr.sin_addr = in_addr(s_addr: in_addr_t(0)) 581 | addr.sin_zero = (0, 0, 0, 0, 0, 0, 0, 0) 582 | return addr 583 | } 584 | 585 | static func setSocketNonBlocking(_ socket: Int32) throws { 586 | if Darwin.fcntl(socket, F_SETFL, Darwin.fcntl(socket, F_GETFL, 0) | O_NONBLOCK) == -1 { 587 | throw AsyncError.setNonBlockFailed(Process.error) 588 | } 589 | } 590 | 591 | static func setSocketNoSigPipe(_ socket: Int32) throws { 592 | var value = 1 593 | if Darwin.setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &value, socklen_t(MemoryLayout.size)) == -1 { 594 | throw AsyncError.setNoSigPipeFailed(Process.error) 595 | } 596 | } 597 | } 598 | 599 | class KernelQueue { 600 | 601 | private var events = Array(repeating: kevent(), count: 256) 602 | private var changes = Array() 603 | 604 | private let queue: Int32 605 | 606 | enum Subscription { case read, write } 607 | enum Event { case read, write, error } 608 | 609 | init() throws { 610 | guard case let queue = kqueue(), queue != -1 else { 611 | throw AsyncError.async(Process.error) 612 | } 613 | self.queue = queue 614 | } 615 | 616 | func subscribe(_ ident: UInt, _ event: Subscription) { 617 | switch event { 618 | case .read : changes.append(self.event(UInt(ident), Int16(EVFILT_READ), UInt16(EV_ADD) | UInt16(EV_ENABLE))) 619 | case .write : changes.append(self.event(UInt(ident), Int16(EVFILT_WRITE), UInt16(EV_ADD) | UInt16(EV_ENABLE))) 620 | } 621 | } 622 | 623 | func unsubscribe(_ ident: UInt, _ event: Subscription) { 624 | switch event { 625 | case .read : changes.append(self.event(UInt(ident), Int16(EVFILT_READ), UInt16(EV_DELETE))) 626 | case .write : changes.append(self.event(UInt(ident), Int16(EVFILT_WRITE), UInt16(EV_DELETE))) 627 | } 628 | } 629 | 630 | func pause(_ ident: UInt, _ event: Subscription) { 631 | switch event { 632 | case .read : changes.append(self.event(UInt(ident), Int16(EVFILT_READ), UInt16(EV_DISABLE))) 633 | case .write : changes.append(self.event(UInt(ident), Int16(EVFILT_WRITE), UInt16(EV_DISABLE))) 634 | } 635 | } 636 | 637 | func resume(_ ident: UInt, _ event: Subscription) { 638 | switch event { 639 | case .read : changes.append(self.event(UInt(ident), Int16(EVFILT_READ), UInt16(EV_ENABLE))) 640 | case .write : changes.append(self.event(UInt(ident), Int16(EVFILT_WRITE), UInt16(EV_ENABLE))) 641 | } 642 | } 643 | 644 | private func event(_ ident: UInt, _ filter: Int16, _ flags: UInt16) -> kevent { 645 | return kevent(ident: ident, filter: filter, flags: flags, fflags: 0, data: 0, udata: nil) 646 | } 647 | 648 | func wait(_ callback: (_ tuple: (event: Event, ident: UInt, data: Int)) throws -> (Void)) throws { 649 | 650 | if !changes.isEmpty { 651 | if kevent(self.queue, &changes, Int32(changes.count), nil, 0, nil) == -1 { 652 | throw AsyncError.async(Process.error) 653 | } 654 | } 655 | 656 | self.changes.removeAll(keepingCapacity: true) 657 | 658 | guard case let count = kevent(self.queue, nil, 0, &events, Int32(events.count), nil), count != -1 else { 659 | throw AsyncError.async(Process.error) 660 | } 661 | 662 | for event in events[0.. String { 21 | if let timestamp = Double(slackTs) { 22 | let date = Date(timeIntervalSince1970: timestamp) 23 | return self.dateFormatter.string(from: date) 24 | } else { 25 | return slackTs 26 | } 27 | } 28 | 29 | func textSpansFor(message: SlackMessage, withContext context: SlackContext, andLinks links: inout [String]) -> [TextSpan] { 30 | 31 | let slackUser = context.user(forId: message.user) ?? SlackUser(id: "", name: "unknown", color: "", presence: .away) 32 | 33 | var spans = [TextSpan]() 34 | 35 | spans.append(TextSpan(self.formatSlackTimestamp(message.ts), withColor: R.color.messageTimeTextColor)) 36 | spans.append(TextSpan(" \(slackUser.name)", withColor: Utils.xterm256Color(forUser: slackUser))) 37 | spans.append(TextSpan(": ", withColor: R.color.messagePrefixTextColor)) 38 | spans.append(contentsOf: self.spansFor(message: message.text, withContext: context, andLinks: &links)) 39 | 40 | if !message.reactions.isEmpty { 41 | let content = "\n " + message.reactions.map({ emojiDecoder.decode( ":" + $0.name + ":" ) + reactionCounter(forCount: $0.count) } ).joined(separator: " ") 42 | 43 | spans.append(TextSpan(content, withColor: R.color.reactionTextColor, withBackground: R.color.defaultBgColor)) 44 | } 45 | 46 | return spans 47 | } 48 | 49 | func reactionCounter(forCount count: Int) -> String { 50 | if count <= 1 { 51 | return "" 52 | } 53 | return String(String(describing: count).map({ c in //TODO - Do it better with 'U+2080 + x' formula. 54 | switch c { 55 | case "0": return "₀" 56 | case "1": return "₁" 57 | case "2": return "₂" 58 | case "3": return "₃" 59 | case "4": return "₄" 60 | case "5": return "₅" 61 | case "6": return "₆" 62 | case "7": return "₇" 63 | case "8": return "₈" 64 | case "9": return "₉" 65 | default : return " " 66 | } 67 | })) 68 | } 69 | 70 | func spansFor(message: String, withContext context: SlackContext, andLinks links: inout [String]) -> [TextSpan] { 71 | 72 | var spans = [TextSpan]() 73 | 74 | for token in self.lex(for: message) { 75 | switch token { 76 | case .plain(let text): 77 | let presentableText = self.emojiDecoder.decode(self.decodeEscapedHTMLEntities(text)) 78 | spans.append(TextSpan(presentableText, withColor: R.color.messageTextColor)) 79 | case .escaped(let encodedText): 80 | guard let first = encodedText.first else { 81 | continue 82 | } 83 | switch first { 84 | case "#": 85 | fallthrough 86 | case "@": 87 | let idStartIndex = encodedText.index(encodedText.startIndex, offsetBy: 1) 88 | if idStartIndex < encodedText.endIndex { 89 | let id = encodedText[idStartIndex..<(encodedText.index(of: "|") ?? encodedText.endIndex)] 90 | if let name = context.name(forId: String(id)) { 91 | spans.append(TextSpan(name, withColor: R.color.mentionTextColor)) 92 | } 93 | } 94 | case "!": 95 | let escapedTokens = encodedText.components(separatedBy: "|") 96 | if let command = escapedTokens.first { 97 | let commandName = escapedTokens.count > 1 ? escapedTokens[1] : command 98 | spans.append(TextSpan(commandName, withColor: R.color.commandTextColor)) 99 | } 100 | default /* link */: 101 | let escapedTokens = encodedText.components(separatedBy: "|") 102 | if let url = escapedTokens.first { 103 | links.append(url) 104 | if let name = escapedTokens.last, escapedTokens.count > 1 { 105 | spans.append(TextSpan(self.decodeEscapedHTMLEntities(name), withColor: R.color.linkTextColor)) 106 | } else { 107 | spans.append(TextSpan(url, withColor: R.color.linkTextColor)) 108 | } 109 | spans.append(TextSpan("[" + String(links.count) + "]", withColor: R.color.messageTextColor)) 110 | } 111 | } 112 | } 113 | } 114 | return spans 115 | } 116 | 117 | enum Token { case plain(String), escaped(String) } 118 | 119 | private func lex(for message: String) -> [Token] { 120 | 121 | var tokens = [Token]() 122 | 123 | var buffer = [Character]() 124 | buffer.reserveCapacity(message.count) 125 | 126 | var plainFlag = true 127 | 128 | for (index, character) in message.enumerated() { 129 | switch character { 130 | case "<": 131 | if !buffer.isEmpty { 132 | tokens.append(.plain(String(buffer))) 133 | } 134 | buffer.removeAll(keepingCapacity: true) 135 | plainFlag = false 136 | case ">": 137 | if !buffer.isEmpty { 138 | tokens.append(.escaped(String(buffer))) 139 | } 140 | buffer.removeAll(keepingCapacity: true) 141 | plainFlag = true 142 | default: 143 | buffer.append(character) 144 | if index == message.count - 1 { 145 | tokens.append(plainFlag ? .plain(String(buffer)) : .escaped(String(buffer))) 146 | } 147 | } 148 | } 149 | 150 | return tokens 151 | } 152 | 153 | private func decodeEscapedHTMLEntities(_ text: String) -> String { 154 | return text 155 | .replacingOccurrences(of: "&", with: "&") 156 | .replacingOccurrences(of: "<", with: "<") 157 | .replacingOccurrences(of: ">", with: ">") 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/SlackChannel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | struct SlackChannel { 10 | 11 | let id: String 12 | let name: String 13 | let members: [String] 14 | let topic: String 15 | let general: Bool 16 | let isMember: Bool 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SlackContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | 10 | class SlackContext { 11 | 12 | var teamName : String 13 | var selfId : String 14 | 15 | var users = [SlackUser]() 16 | var channels = [SlackChannel]() 17 | var groups = [SlackGroup]() 18 | var ims = [SlackIM]() 19 | 20 | init(withTeam team: SlackTeam) { 21 | self.teamName = team.name 22 | self.selfId = team.selfId 23 | self.users.append(contentsOf: team.users) 24 | self.channels.append(contentsOf: team.channels) 25 | self.groups.append(contentsOf: team.groups) 26 | self.ims.append(contentsOf: team.ims) 27 | } 28 | 29 | func suggestRecipient(for selection: String?, unreadIds: Set = [], backwardSearch: Bool) -> (id: String, name: String)? { 30 | 31 | let channels = self.channels.filter({ $0.isMember }).map { ($0.id, $0.name) } 32 | let groups = self.groups.map { ($0.id, $0.name) } 33 | let ims = self.ims.map { im in 34 | (im.id, (self.users.filter({ $0.id == im.user }).first?.name) ?? "") 35 | } 36 | 37 | let candidates = (channels + groups + ims).filter { unreadIds.isEmpty ? true : (unreadIds.contains($0.0) || $0.0 == selection) } 38 | 39 | var suggestion: (id: String, name: String)? = nil 40 | 41 | if let current = candidates.index(where: { $0.0 == selection }) { 42 | if backwardSearch { 43 | let next = current - 1 44 | if next < 0 { 45 | suggestion = candidates.last 46 | } else { 47 | suggestion = candidates[next] 48 | } 49 | } else { 50 | let next = current + 1 51 | if next >= candidates.count { 52 | suggestion = candidates.first 53 | } else { 54 | suggestion = candidates[next] 55 | } 56 | } 57 | } 58 | 59 | return suggestion 60 | } 61 | 62 | func name(forId id: String) -> String? { 63 | 64 | var name = self.channels.filter({ $0.id == id }).first?.name 65 | 66 | if name == nil { 67 | name = self.groups.filter({ $0.id == id }).first?.name 68 | } 69 | 70 | if name == nil { 71 | if let im = self.ims.filter({ $0.id == id }).first { 72 | if let user = self.users.filter({ $0.id == im.user }).first { 73 | name = user.name 74 | } 75 | } 76 | } 77 | 78 | if name == nil { 79 | if let user = self.users.filter({ $0.id == id }).first { 80 | return user.name 81 | } 82 | } 83 | 84 | return name 85 | } 86 | 87 | func user(forId id: String) -> SlackUser? { 88 | return self.users.filter({ $0.id == id }).first 89 | } 90 | 91 | var me: SlackUser? { 92 | return self.users.filter({ $0.id == self.selfId }).first 93 | } 94 | 95 | var defaultChannel: String? { 96 | return self.channels.filter({ $0.general }).first?.id 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Sources/SlackEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | enum SlackEvent { 10 | 11 | case message(SlackMessage) 12 | case messageChanged(SlackMessage) 13 | case messageDeleted(String, String) 14 | case messageReactionAdded(String, String, String) 15 | case messageReactionRemoved(String, String, String) 16 | case hello 17 | case reconnectUrl 18 | case userTyping(String, String) 19 | case channelMarked 20 | case presenceChange(String, SlackUser.Presence) 21 | case unknown(String) 22 | case fileCreated 23 | case filePublic 24 | case fileShared 25 | case fileChange 26 | case prefChange 27 | case groupMarked 28 | case mpimMarked 29 | case imMarked 30 | case userChange 31 | case reply(Int, String) 32 | case teamRename(String) 33 | case desktopNotification(String, String) 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SlackGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | struct SlackGroup { 10 | 11 | let id: String 12 | let name: String 13 | let members: [String] 14 | let topic: String 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SlackIM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | struct SlackIM { 10 | 11 | let id: String 12 | let user: String 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SlackMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | struct SlackMessage { 10 | 11 | let ts: String 12 | let channel: String 13 | let user: String 14 | let text: String 15 | 16 | let reactions: [SlackMessageReaction] 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Sources/SlackMessageReaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlackMessageReaction.swift 3 | // slash 4 | // 5 | // Created by damian on 08.02.2019. 6 | // Copyright © 2019 kolakowski. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct SlackMessageReaction { 12 | 13 | let name: String 14 | let count: Int 15 | let users: [String] 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SlackOAuth2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | enum SlackOAuth2Error: Error { 10 | case error(String) 11 | } 12 | 13 | class SlackOAuth2 { 14 | 15 | static let port = 7777 16 | static let address = "http://localhost:\(SlackOAuth2.port)" 17 | 18 | private let clientId : String 19 | private let clientSecret : String 20 | private let permissions : [String] 21 | 22 | private var accessToken : String? 23 | 24 | init(clientId: String, clientSecret: String, permissions: [String]) { 25 | self.clientId = clientId 26 | self.clientSecret = clientSecret 27 | self.permissions = permissions 28 | } 29 | 30 | func authenticate() throws -> String? { 31 | 32 | let permissionsStr = self.permissions.joined(separator: " ") 33 | 34 | Utils.shell("open", "https://slack.com/oauth/authorize?client_id=\(self.clientId)&scope=\(permissionsStr)&redirect_uri=\(SlackOAuth2.address)") 35 | 36 | let server = try Server(in_port_t(SlackOAuth2.port)) 37 | 38 | var processIncomingRequests = true 39 | 40 | while processIncomingRequests { 41 | 42 | try server.serve { (request, responder) in 43 | 44 | guard request.path == "/" else { 45 | responder(TextResponse(200, "Invalid request.")) 46 | return 47 | } 48 | 49 | guard let code = request.query.first?.1 else { 50 | responder(TextResponse(200, "Could not get the code.")) 51 | return 52 | } 53 | 54 | guard let url = URL(string: "https://slack.com/api/oauth.access?client_id=\(self.clientId)&client_secret=\(self.clientSecret)&code=\(code)&redirect_uri=\(SlackOAuth2.address)") else { 55 | responder(TextResponse(200, "Could not create URL object.")) 56 | return 57 | } 58 | 59 | do { 60 | 61 | let (theData, _) = try URLSession.shared.synchronousDataTask(with: URLRequest(url: url)) 62 | guard let data = theData else { 63 | responder(TextResponse(200, "No response from Slack.")) 64 | return 65 | } 66 | let object = try JSONSerialization.jsonObject(with:data) 67 | 68 | guard let dict = object as? Dictionary else { 69 | responder(TextResponse(200, "Slack's response is not a dictionary \(object).")) 70 | return 71 | } 72 | 73 | guard let accessToken = dict["access_token"] as? String else { 74 | responder(TextResponse(200, "Could not find access token in the response \(dict).")) 75 | return 76 | } 77 | 78 | self.accessToken = accessToken 79 | processIncomingRequests = false 80 | 81 | responder(HtmlResponse(200, R.string.authConfirmation)) 82 | } catch { 83 | responder(TextResponse(200, "Error occured for token request \(error).")) 84 | } 85 | } 86 | } 87 | 88 | 89 | return self.accessToken 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/SlackRealTimeClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | 10 | class SlackRealTimeClient { 11 | 12 | enum Err: Error { 13 | case error(String) 14 | } 15 | 16 | private let websocketClient: WebSocketClient 17 | 18 | init(_ url: String) throws { 19 | 20 | guard let wssUrl = URL(string: url) else { 21 | throw Err.error("Failed to create URL object from: \(url).") 22 | } 23 | 24 | guard let wssHost = wssUrl.host else { 25 | throw Err.error("Could not find host for: \(wssUrl).") 26 | } 27 | 28 | self.websocketClient = try WebSocketClient(wssHost, path: wssUrl.path) 29 | } 30 | 31 | func send(_ channel: String, message: String, replyId: Int) throws { 32 | let jsonData = try JSONSerialization.data(withJSONObject: 33 | ["id": replyId, "type": "message", "channel": channel, "text": message], 34 | options: .prettyPrinted) 35 | try self.websocketClient.writeFrame(opcode: .text, payload: [UInt8](jsonData)) 36 | } 37 | 38 | func waitForEvent() throws -> SlackEvent? { 39 | 40 | guard let frame = try self.websocketClient.waitForFrame() else { 41 | return nil 42 | } 43 | switch frame.opcode { 44 | case .text: 45 | let object = try JSONSerialization.jsonObject(with: Data(frame.payload), options: .init(rawValue: 0)) 46 | guard let dictionary = object as? Dictionary else { 47 | return nil 48 | } 49 | if let replyToId = dictionary["reply_to"] as? Int { 50 | let ts = (dictionary["ts"] as? String) ?? "" 51 | return .reply(replyToId, ts) 52 | } 53 | guard let type = dictionary["type"] as? String else { 54 | return nil 55 | } 56 | switch type { 57 | case "hello": 58 | return .hello 59 | case "message": 60 | return self.handleMessageEvent(dictionary) 61 | case "reconnect_url": 62 | return .reconnectUrl 63 | case "user_typing": 64 | let channel = (dictionary["channel"] as? String) ?? "" 65 | let user = (dictionary["user"] as? String) ?? "" 66 | return .userTyping(channel, user) 67 | case "channel_marked": 68 | return .channelMarked 69 | case "presence_change": 70 | let user = (dictionary["user"] as? String) ?? "" 71 | let presenceValue: String = (dictionary["presence"] as? String) ?? "" 72 | return .presenceChange(user, presenceValue == "active" ? .active : .away) 73 | case "file_created": 74 | return .fileCreated 75 | case "file_public": 76 | return .filePublic 77 | case "file_shared": 78 | return .fileShared 79 | case "file_change": 80 | return .fileChange 81 | case "pref_change": 82 | return .prefChange 83 | case "group_marked": 84 | return .groupMarked 85 | case "mpim_marked": 86 | return .mpimMarked 87 | case "im_marked": 88 | return .imMarked 89 | case "reaction_added": 90 | if let item = dictionary["item"] as? [String: Any] { 91 | if (item["type"] as? String) == "message" { 92 | let reaction = (dictionary["reaction"] as? String) ?? "" 93 | let channel = item["channel"] as? String ?? "" 94 | let ts = item["ts"] as? String ?? "" 95 | return .messageReactionAdded(reaction, channel, ts) 96 | } 97 | } 98 | return nil 99 | case "user_change": 100 | return .userChange 101 | case "team_rename": 102 | let name = (dictionary["name"] as? String) ?? "" 103 | return .teamRename(name) 104 | case "desktop_notification": 105 | let title = ((dictionary["title"] as? String) ?? "") + " " + ((dictionary["subtitle"] as? String) ?? "") 106 | let message = ((dictionary["content"] as? String) ?? "") 107 | return .desktopNotification(title, message) 108 | default: 109 | return .unknown("\(type) - \(dictionary)") 110 | } 111 | case .ping: 112 | try self.websocketClient.writeFrame(opcode: .pong) 113 | default: 114 | return nil 115 | } 116 | return nil 117 | } 118 | 119 | private func handleMessageEvent(_ dictionary: Dictionary) -> SlackEvent { 120 | 121 | let channel = (dictionary["channel"] as? String) ?? "" 122 | let user = (dictionary["user"] as? String) ?? "unknown" 123 | let message = (dictionary["text"] as? String) ?? "" 124 | let ts = (dictionary["ts"] as? String) ?? "" 125 | let subType = (dictionary["subtype"] as? String) ?? "" 126 | 127 | switch subType { 128 | case "message_changed": 129 | if let updatedMessage = (dictionary["message"] as? Dictionary) { 130 | let user = (updatedMessage["user"] as? String) ?? "unknown" 131 | let ts = (updatedMessage["ts"] as? String) ?? "" 132 | let text = (updatedMessage["text"] as? String) ?? "" 133 | return .messageChanged(SlackMessage(ts: ts, channel: channel, user: user, text: text, reactions: [])) 134 | } 135 | case "message_deleted": 136 | let ts = (dictionary["deleted_ts"] as? String) ?? "" 137 | return .messageDeleted(ts, channel) 138 | default:break 139 | } 140 | 141 | return .message(SlackMessage(ts: ts, channel: channel, user: user, text: message, reactions: [])) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/SlackTeam.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | 8 | import Foundation 9 | 10 | struct SlackTeam { 11 | 12 | let selfId: String 13 | let name: String 14 | let users: [SlackUser] 15 | let channels: [SlackChannel] 16 | let groups: [SlackGroup] 17 | let ims: [SlackIM] 18 | let wssUrl: String 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SlackUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | 10 | struct SlackUser { 11 | 12 | enum Presence { 13 | case active, away 14 | } 15 | 16 | let id: String 17 | let name: String 18 | let color: String 19 | let presence: Presence 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SlackWebClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | 8 | import Foundation 9 | 10 | enum SlackWebClientError: Error { 11 | case error(String) 12 | } 13 | 14 | class SlackWebClient { 15 | 16 | private let token: String 17 | 18 | init(authenticatedBy token: String) { 19 | self.token = token 20 | } 21 | 22 | func rtm() throws -> SlackTeam { 23 | return processStartRTMresponse(try self.rpc(forMethod: "rtm.start")) 24 | } 25 | 26 | func history(for channel: String) throws -> [SlackMessage] { 27 | 28 | var method = "channels.history" 29 | 30 | if let firstCharacter = channel.first { 31 | switch firstCharacter { 32 | //TODO - This mapping is rather naive. There should be separated methods for: groups and ims. 33 | case "C": method = "channels.history" 34 | case "G": method = "groups.history" 35 | case "D": method = "im.history" 36 | default: break 37 | } 38 | } 39 | 40 | let response = try self.rpc(forMethod: method, withParams: ["channel": channel]) 41 | 42 | guard let messages = response["messages"] as? Array else { 43 | throw SlackWebClientError.error("No messages array in the response \(response).") 44 | } 45 | 46 | return messages.map { item in 47 | guard let dictionary = item as? Dictionary else { 48 | return SlackMessage(ts: "", channel: "", user: "", text: "", reactions: []) 49 | } 50 | return SlackMessage(ts: (dictionary["ts"] as? String) ?? "", 51 | channel: channel, 52 | user: (dictionary["user"] as? String) ?? "", 53 | text: (dictionary["text"] as? String) ?? "", 54 | reactions: (dictionary["reactions"] as? [[String: Any]])?.map({ item in 55 | return SlackMessageReaction( 56 | name: item["name"] as? String ?? "", 57 | count: item["count"] as? Int ?? 0, 58 | users: item["users"] as? [String] ?? [] 59 | ) 60 | }) ?? [] 61 | ) 62 | } 63 | } 64 | 65 | private func rpc(forMethod rpcMethod: String, withParams params: [String: String] = [:]) throws -> Dictionary { 66 | 67 | var queryParams = [String: String]() 68 | 69 | queryParams["token"] = token 70 | params.forEach { queryParams[$0.key] = $0.value } 71 | 72 | let urlString = ("https://slack.com/api/\(rpcMethod)?") + queryParams.map({ $0 + "=" + $1}).joined(separator: "&") 73 | 74 | guard let url = URL(string: urlString) else { 75 | throw SlackWebClientError.error("Could not create URL object.") 76 | } 77 | 78 | let (theData, _) = try URLSession.shared.synchronousDataTask(with: URLRequest(url: url)) 79 | guard let data = theData else { 80 | throw SlackWebClientError.error("Error receiving data.") 81 | } 82 | let object = try JSONSerialization.jsonObject(with:data) 83 | 84 | guard let dict = object as? Dictionary else { 85 | throw SlackWebClientError.error("\(rpcMethod)'s response is not a dictionary.") 86 | } 87 | 88 | guard (dict["ok"] as? Bool) == true else { 89 | throw SlackWebClientError.error("result not ok \(dict).") 90 | } 91 | 92 | return dict 93 | } 94 | 95 | private func processStartRTMresponse(_ object: Any) -> SlackTeam { 96 | 97 | return SlackTeam( 98 | selfId : object ← "self" ← "id", 99 | name : object ← "team" ← "name", 100 | users: (object ←← "users").map( 101 | { SlackUser(id: $0 ← "id", name: $0 ← "name", color: $0 ← "color", presence: ($0 ← "presence") == "active" ? .active : .away) } 102 | ), 103 | channels: (object ←← "channels").map({ 104 | SlackChannel( 105 | id: $0 ← "id", 106 | name: $0 ← "name", 107 | members: ($0 ←← "members").map({ ($0 as? String) ?? "" }), 108 | topic: $0 ← "topic", 109 | general: $0 ← "is_general", 110 | isMember: $0 ← "is_member" 111 | )} 112 | ), 113 | groups: (object ←← "groups").map( 114 | { SlackGroup(id: $0 ← "id", name: $0 ← "name", 115 | members: ($0 ←← "members").map({ ($0 as? String) ?? "" }), 116 | topic: $0 ← "topic")} 117 | ), 118 | ims: (object ←← "ims").map({ SlackIM(id: $0 ← "id", user: $0 ← "user") }), 119 | wssUrl: object ← "url" 120 | ) 121 | } 122 | } 123 | 124 | precedencegroup LookupSeparatorPrecedence { associativity: left } 125 | 126 | infix operator ← : LookupSeparatorPrecedence 127 | infix operator ←← : LookupSeparatorPrecedence 128 | 129 | func ← (left: Any, right: String) -> [String: Any] { 130 | if let dict = left as? [String: Any] { 131 | return (dict[right] as? [String: Any]) ?? [String: Any]() 132 | } 133 | return [String: Any]() 134 | } 135 | 136 | func ←← (left: Any, right: String) -> [Any] { 137 | if let dict = left as? [String: Any] { 138 | return (dict[right] as? [Any]) ?? [Any]() 139 | } 140 | return [Any]() 141 | } 142 | 143 | func ← (left: Any, right: String) -> String? { 144 | if let dict = left as? [String: Any] { 145 | return (dict[right] as? String) 146 | } 147 | return nil 148 | } 149 | 150 | func ← (left: Any, right: String) -> String { 151 | if let dict = left as? [String: Any] { 152 | return (dict[right] as? String) ?? "" 153 | } 154 | return "" 155 | } 156 | 157 | func ← (left: Any, right: String) -> Bool? { 158 | if let dict = left as? [String: Any] { 159 | return (dict[right] as? Bool) 160 | } 161 | return nil 162 | } 163 | 164 | func ← (left: Any, right: String) -> Bool { 165 | if let dict = left as? [String: Any] { 166 | return (dict[right] as? Bool) ?? false 167 | } 168 | return false 169 | } 170 | 171 | func ← (left: [String: Any], right: String) -> [String: Any] { 172 | return (left[right] as? [String: Any]) ?? [String: Any]() 173 | } 174 | 175 | func ← (left: [String: Any], right: String) -> Int? { 176 | return (left[right] as? Int) ?? nil 177 | } 178 | 179 | func ← (left: [String: Any], right: String) -> Int { 180 | return (left[right] as? Int) ?? 0 181 | } 182 | 183 | func ← (left: [String: Any], right: String) -> String? { 184 | return (left[right] as? String) ?? nil 185 | } 186 | 187 | func ← (left: [String: Any], right: String) -> String { 188 | return (left[right] as? String) ?? "" 189 | } 190 | 191 | func ← (left: [String: Any], right: String) -> Bool? { 192 | return (left[right] as? Bool) 193 | } 194 | 195 | func ← (left: [String: Any], right: String) -> Bool { 196 | return (left[right] as? Bool) ?? false 197 | } 198 | -------------------------------------------------------------------------------- /Sources/Socket.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 2 | import Darwin 3 | #elseif os(Linux) 4 | import Glibc 5 | #endif 6 | 7 | struct Socket { 8 | static func read(_ socket:Int32, _ buffer: UnsafeMutableRawPointer, _ size:Int) -> Int { 9 | #if os(macOS) 10 | return Darwin.read(socket, buffer, size) 11 | #elseif os(Linux) 12 | return Glibc.read(socket, buffer, size) 13 | #endif 14 | } 15 | 16 | static func write(_ socket:Int32, _ buffer: Array, _ size:Int) -> Int { 17 | #if os(macOS) 18 | return Darwin.write(socket, buffer, size) 19 | #elseif os(Linux) 20 | return Glibc.write(socket, buffer, size) 21 | #endif 22 | } 23 | 24 | static func close(_ socket:Int32) -> Int32 { 25 | #if os(macOS) 26 | return Darwin.close(socket) 27 | #elseif os(Linux) 28 | return Glibc.close(socket) 29 | #endif 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/TLSSocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | #if os(OSX) || os(iOS) 10 | 11 | enum TLSSocketError: Error { 12 | case error(String) 13 | } 14 | 15 | class TLSSocket { 16 | 17 | private var socket: Int32 18 | private let sslContext: SSLContext 19 | 20 | init(_ address: String, port: Int = 443) throws { 21 | 22 | guard let context = SSLCreateContext(nil, .clientSide, .streamType) else { 23 | throw TLSSocketError.error("SSLCreateContext returned null.") 24 | } 25 | 26 | self.sslContext = context 27 | 28 | let socket = Darwin.socket(AF_INET, SOCK_STREAM, 0) 29 | 30 | guard socket != -1 else { 31 | throw TLSSocketError.error("Darwin.socket failed: \(errno)") 32 | } 33 | 34 | self.socket = socket 35 | 36 | var addr = sockaddr_in() 37 | addr.sin_len = __uint8_t(MemoryLayout.size) 38 | addr.sin_family = sa_family_t(AF_INET) 39 | addr.sin_port = UInt16(port).bigEndian 40 | 41 | guard inet_pton(AF_INET, address, &(addr.sin_addr)) == 1 else { 42 | let _ = Darwin.close(self.socket) 43 | throw TLSSocketError.error("inet_pton failed.") 44 | } 45 | 46 | if withUnsafePointer(to: &addr, { 47 | Darwin.connect(socket, UnsafePointer(OpaquePointer($0)), socklen_t(MemoryLayout.size)) 48 | }) == -1 { 49 | let _ = Darwin.close(self.socket) 50 | throw TLSSocketError.error("Darwin.connect failed: \(errno)") 51 | } 52 | 53 | SSLSetIOFuncs(context, sslRead, sslWrite) 54 | 55 | guard SSLSetConnection(context, &self.socket) == noErr else { 56 | let _ = Darwin.close(self.socket) 57 | throw TLSSocketError.error("SSLSetConnection failed.") 58 | } 59 | 60 | let handshakeResult = SSLHandshake(context) 61 | 62 | guard handshakeResult == noErr else { 63 | let _ = Darwin.close(self.socket) 64 | throw TLSSocketError.error("SSLHandshake failed: \(handshakeResult)") 65 | } 66 | } 67 | 68 | func close() { 69 | SSLClose(self.sslContext) 70 | let _ = Darwin.close(self.socket) 71 | } 72 | 73 | func writeData(_ data: [UInt8]) throws { 74 | var processed = 0 75 | let result = SSLWrite(self.sslContext, data, data.count, &processed) 76 | guard result == noErr else { 77 | throw TLSSocketError.error("SSLWrite failed: \(result)") 78 | } 79 | } 80 | 81 | func readData() throws -> [UInt8] { 82 | var processed = 0 83 | var data = [UInt8](repeating: 0, count: 1024) 84 | let result = SSLRead(self.sslContext, &data, data.count, &processed) 85 | guard result == noErr else { 86 | throw TLSSocketError.error("SSLRead failed: \(result)") 87 | } 88 | data.removeLast(data.count - processed) 89 | return data 90 | } 91 | } 92 | 93 | func sslRead(_ socketRef: SSLConnectionRef, _ data: UnsafeMutableRawPointer, _ length: UnsafeMutablePointer) -> OSStatus { 94 | let socket = socketRef.load(as: Int32.self) 95 | var n = 0 96 | while n < length.pointee { 97 | let result = read(socket, data + n, length.pointee - n) 98 | if result <= 0 { 99 | if result == Int(ENOENT) { 100 | return errSSLClosedGraceful 101 | } 102 | if result == Int(ECONNRESET) { 103 | return errSSLClosedAbort 104 | } 105 | return OSStatus(ioErr) 106 | } 107 | n += result 108 | } 109 | length.pointee = n 110 | return noErr 111 | } 112 | 113 | func sslWrite(_ socketRef: SSLConnectionRef, _ data: UnsafeRawPointer, _ length: UnsafeMutablePointer) -> OSStatus { 114 | let socket = socketRef.load(as: Int32.self) 115 | var n = 0 116 | while n < length.pointee { 117 | let result = write(socket, data + n, length.pointee - n) 118 | if result <= 0 { 119 | if result == Int(EPIPE) { 120 | return errSSLClosedAbort 121 | } 122 | if result == Int(ECONNRESET) { 123 | return errSSLClosedAbort 124 | } 125 | return OSStatus(ioErr) 126 | } 127 | n += result 128 | } 129 | length.pointee = n 130 | return noErr 131 | } 132 | 133 | #endif 134 | -------------------------------------------------------------------------------- /Sources/TerminalCanvas.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | class TerminalCanvas { 10 | 11 | var buffer = [UInt8]() 12 | 13 | init() { 14 | self.buffer.reserveCapacity(4_000) 15 | } 16 | 17 | @discardableResult func clear() -> TerminalCanvas { 18 | self.buffer.removeAll(keepingCapacity: true) 19 | return self 20 | } 21 | 22 | @discardableResult func cursor(_ x: Int, _ y: Int) -> TerminalCanvas { 23 | self.buffer.append(contentsOf: "\u{001B}[\(y);\(x)H".utf8) 24 | return self 25 | } 26 | 27 | @discardableResult func hideCursor() -> TerminalCanvas { 28 | self.buffer.append(contentsOf: "\u{001B}[?25l".utf8) 29 | return self 30 | } 31 | 32 | @discardableResult func showCursor() -> TerminalCanvas { 33 | self.buffer.append(contentsOf: "\u{001B}[?25h".utf8) 34 | return self 35 | } 36 | 37 | @discardableResult func clean() -> TerminalCanvas { 38 | buffer.append(contentsOf: "\u{001B}[0m\u{001B}[2J".utf8) 39 | return self 40 | } 41 | 42 | @discardableResult func color(_ color: Int) -> TerminalCanvas { 43 | if color < 0 { 44 | buffer.append(contentsOf: "\u{001B}[39m".utf8) 45 | } else { 46 | buffer.append(contentsOf: "\u{001B}[38;5;\(color)m".utf8) 47 | } 48 | return self 49 | } 50 | 51 | @discardableResult func background(_ color: Int) -> TerminalCanvas { 52 | if color < 0 { 53 | buffer.append(contentsOf: "\u{001B}[49m".utf8) 54 | } else { 55 | buffer.append(contentsOf: "\u{001B}[48;5;\(color)m".utf8) 56 | } 57 | return self 58 | } 59 | 60 | @discardableResult func blink(_ flag: Bool) -> TerminalCanvas { 61 | self.buffer.append(contentsOf: "\u{001B}[\(flag ? "5" : "25")m".utf8) 62 | return self 63 | } 64 | 65 | @discardableResult func text(_ text: String) -> TerminalCanvas { 66 | buffer.append(contentsOf: text.utf8) 67 | return self 68 | } 69 | 70 | @discardableResult func reset() -> TerminalCanvas { 71 | buffer.append(contentsOf: "\u{001B}[0m".utf8) 72 | return self 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/TerminalDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | enum ControlKey { 10 | 11 | case null 12 | case ctrlC 13 | case ctrlD 14 | case ctrlF 15 | case ctrlH 16 | case tab(Bool) 17 | case cr 18 | case ctrlL 19 | case enter 20 | case ctrlQ 21 | case ctrlS 22 | case ctrlU 23 | case esc 24 | case backspace 25 | case arrowUp 26 | case arrowDown 27 | case arrowLeft 28 | case arrowRight 29 | 30 | case other(Character) 31 | } 32 | 33 | class TerminalDevice { 34 | 35 | enum TerminalError: Error { 36 | case error(String) 37 | } 38 | 39 | private var originalTermios = termios() 40 | 41 | init() throws { 42 | guard tcgetattr(fileno(stdin), &self.originalTermios) == 0 else { 43 | throw TerminalError.error("Could not load current terminal config: \(errno)") 44 | } 45 | try self.terminalEnableRawMode() 46 | 47 | self.flush(TerminalCanvas().clean().buffer) 48 | } 49 | 50 | var size: (width: Int, height: Int) { 51 | var w = winsize() 52 | guard ioctl(fileno(stdout), TIOCGWINSZ, &w) == 0 else { 53 | exit(1) 54 | } 55 | return (Int(w.ws_col), Int(w.ws_row)) 56 | } 57 | 58 | func key() -> ControlKey { 59 | 60 | let c = getwchar() 61 | 62 | switch c { 63 | 64 | case 0: return .null 65 | case 3: return .ctrlC 66 | case 4: return .ctrlD 67 | case 6: return .ctrlF 68 | case 8: return .ctrlH 69 | case 9: return .tab(false) 70 | case 10: return .cr 71 | case 12: return .ctrlL 72 | case 13: return .enter 73 | case 17: return .ctrlQ 74 | case 19: return .ctrlS 75 | case 21: return .ctrlU 76 | 77 | case 27: 78 | 79 | let c1 = getwchar() 80 | let c2 = getwchar() 81 | 82 | if c1 == 91 { 83 | switch c2 { 84 | case 65: return .arrowUp 85 | case 66: return .arrowDown 86 | case 68: return .arrowLeft 87 | case 67: return .arrowRight 88 | case 90: return .tab(true) 89 | default: break 90 | } 91 | } 92 | 93 | return .esc 94 | 95 | case 127: return .backspace 96 | 97 | default: return .other(Character(UnicodeScalar(Int(c))!)) 98 | } 99 | } 100 | 101 | func flush(_ buffer: [UInt8]) { 102 | write(fileno(stdout), buffer, buffer.count) 103 | } 104 | 105 | func reset() throws { 106 | guard tcsetattr(fileno(stdin), TCSAFLUSH, &self.originalTermios) == 0 else { 107 | throw TerminalError.error("Could not revert the original mode: \(errno)") 108 | } 109 | self.flush(TerminalCanvas().clean().cursor(1, 1).showCursor().buffer) 110 | } 111 | 112 | private func terminalEnableRawMode() throws { 113 | 114 | var rawModeTermios = termios() 115 | 116 | memcpy(&rawModeTermios, &self.originalTermios, MemoryLayout.size) 117 | 118 | rawModeTermios.c_lflag = UInt(Int32(rawModeTermios.c_lflag) 119 | 120 | & (~(ECHO | ECHONL | ICANON | IEXTEN | ISIG))) 121 | 122 | rawModeTermios.c_iflag = UInt(Int32(rawModeTermios.c_iflag) 123 | 124 | & (~(IGNBRK | BRKINT | INLCR | ICRNL | INPCK | PARMRK | ISTRIP | IXON | IUTF8))) 125 | 126 | rawModeTermios.c_cflag = UInt(Int32(rawModeTermios.c_cflag) & CS8) 127 | rawModeTermios.c_oflag = 0 128 | 129 | rawModeTermios.c_cc.17 /*[VTIME]*/ = 0 130 | rawModeTermios.c_cc.16 /*[VMIN] */ = 1 131 | 132 | guard tcsetattr(fileno(stdin), TCSAFLUSH, &rawModeTermios) == 0 else { 133 | throw TerminalError.error("Could not enable raw mode: \(errno)") 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/TextLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | 10 | struct TextSpan: ExpressibleByStringLiteral { 11 | 12 | let text: String 13 | 14 | let textColor: Int 15 | 16 | let backgroundColor: Int 17 | 18 | init(stringLiteral value: String) { 19 | self.text = value 20 | self.textColor = R.color.defaulTextColor 21 | self.backgroundColor = R.color.defaultBgColor 22 | } 23 | 24 | init(extendedGraphemeClusterLiteral value: String) { 25 | self.text = value 26 | self.textColor = R.color.defaulTextColor 27 | self.backgroundColor = R.color.defaultBgColor 28 | } 29 | 30 | init(unicodeScalarLiteral value: String) { 31 | self.text = value 32 | self.textColor = R.color.defaulTextColor 33 | self.backgroundColor = R.color.defaultBgColor 34 | } 35 | 36 | init(_ text: String, withColor: Int = R.color.defaulTextColor, withBackground: Int = R.color.defaultBgColor) { 37 | self.text = text 38 | self.textColor = withColor 39 | self.backgroundColor = withBackground 40 | } 41 | } 42 | 43 | class TextLayout { 44 | 45 | let canvas = TerminalCanvas() 46 | 47 | func layout(_ spans: [TextSpan], alignToWidth width: Int, highlightColor: Int? = nil) -> [[UInt8]] { 48 | 49 | canvas.clear() 50 | 51 | var counter = 0 52 | var lines = [[UInt8]]() 53 | 54 | for span in spans { 55 | 56 | canvas.background(highlightColor ?? span.backgroundColor) 57 | canvas.color(span.textColor) 58 | 59 | for character in span.text { 60 | if character == "\n" { 61 | canvas.text(String(repeating: " ", count: width - counter)) 62 | counter = 0 63 | lines.append(canvas.buffer) 64 | canvas.clear() 65 | canvas.background(highlightColor ?? span.backgroundColor) 66 | canvas.color(span.textColor) 67 | } else { 68 | canvas.text(String(character)) 69 | counter = counter + 1 70 | if counter == width { 71 | counter = 0 72 | lines.append(canvas.buffer) 73 | canvas.clear() 74 | canvas.background(highlightColor ?? span.backgroundColor) 75 | canvas.color(span.textColor) 76 | } 77 | } 78 | 79 | } 80 | } 81 | 82 | if counter < width { 83 | canvas.text(String(repeating: " ", count: width - counter)) 84 | lines.append(canvas.buffer) 85 | } 86 | 87 | return lines 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Sources/URLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | extension URLSession { 10 | 11 | /// Synchonized version of dataTask(with URLRequest) 12 | func synchronousDataTask(with request: URLRequest) throws -> (data: Data?, response: HTTPURLResponse?) { 13 | 14 | let semaphore = DispatchSemaphore(value: 0) 15 | 16 | var data: Data? 17 | var response: URLResponse? 18 | var error: Error? 19 | 20 | URLSession.shared.dataTask(with: request) { (theData, theResponse, theError) -> Void in 21 | // extract information from callback 22 | data = theData 23 | response = theResponse 24 | error = theError 25 | 26 | // wake semaphore 27 | semaphore.signal() 28 | 29 | }.resume() 30 | 31 | // wait until signaled 32 | _ = semaphore.wait(timeout: .distantFuture) 33 | 34 | // do we have an error? 35 | if let error = error { 36 | throw error 37 | } 38 | 39 | return (data: data, response: response as! HTTPURLResponse?) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/UserInputView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | 10 | class UserInputView { 11 | 12 | private let renderer = TerminalCanvas() 13 | 14 | private let terminalDevice: TerminalDevice 15 | 16 | init(_ device: TerminalDevice) { 17 | self.terminalDevice = device 18 | } 19 | 20 | var placeholder = R.string.inputPlaceholder 21 | 22 | var input = "" 23 | 24 | var cursor = 0 25 | 26 | func draw() { 27 | 28 | let size = self.terminalDevice.size 29 | 30 | guard size.width > 0 && size.height > 0 else { 31 | return 32 | } 33 | 34 | self.renderer 35 | .clear() 36 | .hideCursor() 37 | .cursor(R.dimen.channelsListWidth + 1, size.height - 1) 38 | .background(R.color.defaultBgColor) 39 | .color(R.color.channelListBgColor) 40 | .text(String(repeating: "─", count: size.width-R.dimen.channelsListWidth)) 41 | .cursor(R.dimen.channelsListWidth + 1, size.height) 42 | 43 | if self.input.isEmpty { 44 | self.renderer 45 | .color(R.color.userInputPlaceholderTextColor) 46 | .background(R.color.userInputBackgorundColor) 47 | .text((" " + placeholder).padding(toLength: size.width - R.dimen.channelsListWidth, withPad: " ", startingAt: 0)) 48 | .cursor(R.dimen.channelsListWidth + 2, size.height) 49 | } else { 50 | self.renderer 51 | .color(R.color.userInputTextColor) 52 | .background(R.color.userInputBackgorundColor) 53 | .text((" " + input).padding(toLength: size.width - R.dimen.channelsListWidth, withPad: " ", startingAt: 0)) 54 | .cursor(R.dimen.channelsListWidth + 2 + cursor, size.height) 55 | } 56 | 57 | self.renderer.showCursor() 58 | 59 | self.terminalDevice.flush(renderer.buffer) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | 10 | class Utils { 11 | 12 | class func shell(_ args: String...) { 13 | let task = Process() 14 | task.launchPath = "/usr/bin/env" 15 | task.arguments = args 16 | task.launch() 17 | } 18 | 19 | class func xterm256Color(forUser user: SlackUser) -> Int { 20 | // Slack API provides a True-Color value for every user (example: 4b3a5a). Terminal supports only 256 colors. 21 | // Instead of using complex alghoritm for finding the closet color from 256 palette, just get modulo 255 value. 22 | return (Int(user.color, radix: 16) ?? R.color.messageAuthorTextColor) % 255 23 | } 24 | } -------------------------------------------------------------------------------- /Sources/WebSocketClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | class WebSocketClient { 10 | 11 | enum Err: Error { 12 | 13 | case unknownOpCode(String) 14 | case unMaskedFrame 15 | case notImplemented(String) 16 | case invalidFrameLength(String) 17 | case io(String) 18 | } 19 | 20 | enum OpCode: UInt8 { 21 | 22 | case `continue` = 0x00 23 | case close = 0x08 24 | case ping = 0x09 25 | case pong = 0x0A 26 | case text = 0x01 27 | case binary = 0x02 28 | } 29 | 30 | struct Frame { 31 | 32 | let opcode: OpCode 33 | let payload: [UInt8] 34 | } 35 | 36 | private let mask: [UInt8] 37 | private let socket: TLSSocket 38 | 39 | private var inputBuffer = [UInt8]() 40 | 41 | init(_ host: String, path: String) throws { 42 | 43 | self.mask = WebSocketClient.provideRandomValues(4) 44 | 45 | self.socket = try TLSSocket(try WebSocketClient.addressForHost(host)) 46 | 47 | let secWebSocketKey = Data(WebSocketClient.provideRandomValues(16)).base64EncodedString() 48 | 49 | let handshakeRequest = [UInt8](( 50 | "GET \(path)?encoding=text HTTP/1.1\r\n" + 51 | "Host: \(host)\r\n" + 52 | "Pragma:no-cache\r\n" + 53 | "Upgrade: websocket\r\n" + 54 | "Connection: Upgrade\r\n" + 55 | "Cache-Control: no-cache\r\n" + 56 | "Sec-WebSocket-Version: 13\r\n" + 57 | "Sec-WebSocket-Key: \(secWebSocketKey)\r\n\r\n").utf8) 58 | 59 | try self.socket.writeData(handshakeRequest) 60 | 61 | //TODO: parse http response in a better way to validate returned Sec-WebSocket-Accept. 62 | 63 | skipHeaders: while true { 64 | let chunk = try self.socket.readData() 65 | var iteratpr = chunk.makeIterator() 66 | while let b = iteratpr.next() { 67 | if b != 13 { inputBuffer.append(b) } 68 | if inputBuffer.count >= 2 && inputBuffer.last == 10 && inputBuffer[inputBuffer.endIndex - 2] == 10 { 69 | inputBuffer.removeAll(keepingCapacity: true) 70 | inputBuffer.append(contentsOf: iteratpr) 71 | break skipHeaders 72 | } 73 | } 74 | } 75 | } 76 | 77 | func writeFrame(fin: Bool = true, opcode: OpCode, payload: [UInt8] = []) throws { 78 | var data = [UInt8]() 79 | data.append(UInt8(fin ? 0x80 : 0x00) | opcode.rawValue) 80 | data.append(contentsOf: encodeLengthAndMaskFlag(UInt64(payload.count), mask: self.mask)) 81 | data.append(contentsOf: payload.enumerated().map { item in 82 | item.element ^ self.mask[item.offset % self.mask.count] 83 | }) 84 | try self.socket.writeData(data) 85 | } 86 | 87 | private class func provideRandomValues(_ count: Int) -> [UInt8] { 88 | var result = [UInt8]() 89 | for _ in 0.. [UInt8] { 96 | let encodedLngth = UInt8(mask != nil ? 0x80 : 0x00) 97 | var encodedBytes = [UInt8]() 98 | switch len { 99 | case 0...125: 100 | encodedBytes.append(encodedLngth | UInt8(len)); 101 | case 126...UInt64(UINT16_MAX): 102 | encodedBytes.append(encodedLngth | 0x7E); 103 | encodedBytes.append(UInt8(len >> 8 & 0xFF)); 104 | encodedBytes.append(UInt8(len >> 0 & 0xFF)); 105 | default: 106 | encodedBytes.append(encodedLngth | 0x7F); 107 | encodedBytes.append(UInt8(len >> 56 & 0xFF)); 108 | encodedBytes.append(UInt8(len >> 48 & 0xFF)); 109 | encodedBytes.append(UInt8(len >> 40 & 0xFF)); 110 | encodedBytes.append(UInt8(len >> 32 & 0xFF)); 111 | encodedBytes.append(UInt8(len >> 24 & 0xFF)); 112 | encodedBytes.append(UInt8(len >> 16 & 0xFF)); 113 | encodedBytes.append(UInt8(len >> 08 & 0xFF)); 114 | encodedBytes.append(UInt8(len >> 00 & 0xFF)); 115 | } 116 | if let mask = mask { 117 | encodedBytes.append(contentsOf: mask) 118 | } 119 | return encodedBytes 120 | } 121 | 122 | func waitForFrame() throws -> Frame? { 123 | 124 | // Handle remaining frames after the handshake. 125 | 126 | if let frame = try self.lookfForFrame() { 127 | return frame 128 | } 129 | 130 | inputBuffer.append(contentsOf: try self.socket.readData()) 131 | 132 | if let frame = try self.lookfForFrame() { 133 | return frame 134 | } 135 | 136 | return nil 137 | } 138 | 139 | private func lookfForFrame() throws -> Frame? { 140 | 141 | guard inputBuffer.count > 1 else { return nil } 142 | 143 | let _ /* fin flag */ = inputBuffer[0] & 0x80 != 0 144 | let opc = inputBuffer[0] & 0x0F 145 | 146 | guard let opcode = OpCode(rawValue: opc) else { 147 | // "If an unknown opcode is received, the receiving endpoint MUST _Fail the WebSocket Connection_." 148 | // http://tools.ietf.org/html/rfc6455#section-5.2 ( Page 29 ) 149 | throw Err.unknownOpCode("\(opc)") 150 | } 151 | 152 | var offset = 2 153 | var len = UInt64(0) 154 | 155 | switch UInt64(inputBuffer[1] & 0x7F) { 156 | case let short where short < 0x7E: 157 | len = short 158 | case 0x7E: 159 | guard inputBuffer.count > 3 else { return nil } 160 | len = UInt64(littleEndian: UInt64(inputBuffer[2]) << 8 | UInt64(inputBuffer[3])) 161 | offset = 4 162 | case 0x7F: 163 | guard inputBuffer.count > 9 else { return nil } 164 | let byte2 = UInt64(inputBuffer[2]) << 54 165 | let byte3 = UInt64(inputBuffer[3]) << 48 166 | let byte4 = UInt64(inputBuffer[4]) << 40 167 | let byte5 = UInt64(inputBuffer[5]) << 32 168 | let byte6 = UInt64(inputBuffer[6]) << 24 169 | let byte7 = UInt64(inputBuffer[7]) << 16 170 | let byte8 = UInt64(inputBuffer[8]) << 8 171 | let byte9 = UInt64(inputBuffer[9]) 172 | len = UInt64(littleEndian: byte2 | byte3 | byte4 | byte5 | byte6 | byte7 | byte8 | byte9) 173 | offset = 10 174 | default: 175 | throw Err.invalidFrameLength("Not allowed frame length: \(len)") 176 | } 177 | 178 | let masked = (inputBuffer[1] & 0x80) != 0 179 | 180 | guard (len + UInt64(offset) + (masked ? 4 : 0)) <= UInt64(inputBuffer.count) else { 181 | return nil 182 | } 183 | 184 | if masked { 185 | 186 | let mask = [inputBuffer[offset], inputBuffer[offset+1], inputBuffer[offset+2], inputBuffer[offset+3]] 187 | 188 | offset = offset + mask.count 189 | 190 | let payload = inputBuffer[offset..<(offset + Int(len /* //TODO fix Int64/Int conversion */))] 191 | .enumerated() 192 | .map { 193 | $0.element ^ mask[Int($0.offset % 4)] 194 | } 195 | 196 | inputBuffer.removeFirst(offset+Int(len)) 197 | 198 | return Frame(opcode: opcode, payload: payload) 199 | 200 | } else { 201 | 202 | let payload = [UInt8](inputBuffer[offset..<(offset + Int(len /* //TODO fix Int64/Int conversion */))]) 203 | 204 | inputBuffer.removeFirst(offset+Int(len)) 205 | 206 | return Frame(opcode: opcode, payload: payload) 207 | } 208 | } 209 | 210 | private static func addressForHost(_ host: String) throws -> String { 211 | guard let info = host.withCString({ gethostbyname($0) }) else { 212 | throw Err.io("Could not find address for \(host): gethostbyname failed.") 213 | } 214 | guard let first = info.pointee.h_addr_list[0] else { 215 | throw Err.io("Could not find address for \(host): empty list.") 216 | } 217 | var buffer = [Int8](repeating: 0, count: 256) 218 | guard inet_ntop(AF_INET, first, &buffer, socklen_t(buffer.count)) != nil else { 219 | throw Err.io("Could not find address for \(host): inet_ntop failed \(errno).") 220 | } 221 | return String(cString: buffer) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /Sources/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // slash 3 | // 4 | // Copyright © 2016 slash Corp. All rights reserved. 5 | // 6 | 7 | import Foundation 8 | 9 | setlocale(LC_CTYPE,"UTF-8") 10 | 11 | let device = try TerminalDevice() 12 | 13 | CrashReporter.watch(usingDevice: device) 14 | 15 | var token: String! 16 | 17 | if CommandLine.arguments.count > 1 { 18 | 19 | token = CommandLine.arguments[1] 20 | 21 | } else { 22 | 23 | device.flush(TerminalCanvas() 24 | .clean() 25 | .cursor(1, 1) 26 | .text(String(format: R.string.authHelpMessage, SlackOAuth2.address)).buffer) 27 | 28 | let slackAuthenticator = SlackOAuth2(clientId: "2296647491.109731100693", clientSecret: "db81eea6c974916693ab746775dbc096", permissions: ["client"]) 29 | 30 | token = try slackAuthenticator.authenticate() 31 | 32 | device.flush(TerminalCanvas().clean().buffer) 33 | } 34 | 35 | let application = try Application(usingDevice: device, authenticatedBy: token) 36 | 37 | signal(SIGWINCH) { _ in 38 | application.notifyTerminalSizeHasChanged() 39 | } 40 | 41 | application.run() 42 | 43 | -------------------------------------------------------------------------------- /slash.rb: -------------------------------------------------------------------------------- 1 | class Slash < Formula 2 | desc "Slack terminal client written in Swift." 3 | homepage "https://github.com/slash-hq/slash" 4 | url "https://github.com/slash-hq/slash/archive/0.1.0.tar.gz" 5 | sha256 "8a709579ffba7c47b1e1975bb418d72ecbd542539d4cd6f7a72d876808bfbdb2" 6 | 7 | depends_on :xcode 8 | def install 9 | xcodebuild "-workspace", "slash.xcodeproj/project.xcworkspace", "-derivedDataPath", "prefix.to_s", "-configuration", "Release", "-scheme", "slash", "SYMROOT=#{prefix}/Build" 10 | bin.install(prefix + "Build/Release/slash") 11 | end 12 | 13 | test do 14 | system "false" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /slash.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "build_systems": [ 3 | { 4 | "shell_cmd": "swift build -v", 5 | "name": "Sublime SPM Build for Swift." 6 | } 7 | ], 8 | "folders": [ 9 | { 10 | "path": ".", 11 | "follow_symlinks": true 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /slash.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2DB32B79220D933300D23E57 /* SlackMessageReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB32B78220D933300D23E57 /* SlackMessageReaction.swift */; }; 11 | 7C92EB571E03F3C900F92267 /* TerminalCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB541E03F3C900F92267 /* TerminalCanvas.swift */; }; 12 | 7C92EB581E03F3C900F92267 /* TerminalDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB551E03F3C900F92267 /* TerminalDevice.swift */; }; 13 | 7C92EB591E03F3C900F92267 /* TextLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB561E03F3C900F92267 /* TextLayout.swift */; }; 14 | 7C92EB651E03F3ED00F92267 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB5A1E03F3ED00F92267 /* Application.swift */; }; 15 | 7C92EB661E03F3ED00F92267 /* ChannelsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB5B1E03F3ED00F92267 /* ChannelsListView.swift */; }; 16 | 7C92EB671E03F3ED00F92267 /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB5C1E03F3ED00F92267 /* CrashReporter.swift */; }; 17 | 7C92EB681E03F3ED00F92267 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB5D1E03F3ED00F92267 /* main.swift */; }; 18 | 7C92EB691E03F3ED00F92267 /* MessagesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB5E1E03F3ED00F92267 /* MessagesListView.swift */; }; 19 | 7C92EB6A1E03F3ED00F92267 /* R.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB5F1E03F3ED00F92267 /* R.swift */; }; 20 | 7C92EB6B1E03F3ED00F92267 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB601E03F3ED00F92267 /* Server.swift */; }; 21 | 7C92EB6C1E03F3ED00F92267 /* SlackAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB611E03F3ED00F92267 /* SlackAdapter.swift */; }; 22 | 7C92EB6D1E03F3ED00F92267 /* SlackContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB621E03F3ED00F92267 /* SlackContext.swift */; }; 23 | 7C92EB6E1E03F3ED00F92267 /* UserInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB631E03F3ED00F92267 /* UserInputView.swift */; }; 24 | 7C92EB6F1E03F3ED00F92267 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB641E03F3ED00F92267 /* Utils.swift */; }; 25 | 7C92EB7D1E03F3FC00F92267 /* SlackChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB701E03F3FC00F92267 /* SlackChannel.swift */; }; 26 | 7C92EB7E1E03F3FC00F92267 /* SlackEmojiDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB711E03F3FC00F92267 /* SlackEmojiDecoder.swift */; }; 27 | 7C92EB7F1E03F3FC00F92267 /* SlackEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB721E03F3FC00F92267 /* SlackEvent.swift */; }; 28 | 7C92EB801E03F3FC00F92267 /* SlackGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB731E03F3FC00F92267 /* SlackGroup.swift */; }; 29 | 7C92EB811E03F3FC00F92267 /* SlackIM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB741E03F3FC00F92267 /* SlackIM.swift */; }; 30 | 7C92EB821E03F3FC00F92267 /* SlackMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB751E03F3FC00F92267 /* SlackMessage.swift */; }; 31 | 7C92EB831E03F3FC00F92267 /* SlackOAuth2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB761E03F3FC00F92267 /* SlackOAuth2.swift */; }; 32 | 7C92EB841E03F3FC00F92267 /* SlackRealTimeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB771E03F3FC00F92267 /* SlackRealTimeClient.swift */; }; 33 | 7C92EB851E03F3FC00F92267 /* SlackTeam.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB781E03F3FC00F92267 /* SlackTeam.swift */; }; 34 | 7C92EB861E03F3FC00F92267 /* SlackUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB791E03F3FC00F92267 /* SlackUser.swift */; }; 35 | 7C92EB871E03F3FC00F92267 /* SlackWebClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB7A1E03F3FC00F92267 /* SlackWebClient.swift */; }; 36 | 7C92EB881E03F3FC00F92267 /* TLSSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB7B1E03F3FC00F92267 /* TLSSocket.swift */; }; 37 | 7C92EB891E03F3FC00F92267 /* WebSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C92EB7C1E03F3FC00F92267 /* WebSocketClient.swift */; }; 38 | 7C9FE5F11E18F9B900FB8166 /* Socket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9FE5F01E18F9B900FB8166 /* Socket.swift */; }; 39 | D42C5DF71E156BEE008CDA3C /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42C5DF61E156BEE008CDA3C /* URLSession.swift */; }; 40 | /* End PBXBuildFile section */ 41 | 42 | /* Begin PBXCopyFilesBuildPhase section */ 43 | 7CBB81F01DC12E8B00DC0334 /* CopyFiles */ = { 44 | isa = PBXCopyFilesBuildPhase; 45 | buildActionMask = 2147483647; 46 | dstPath = /usr/share/man/man1/; 47 | dstSubfolderSpec = 0; 48 | files = ( 49 | ); 50 | runOnlyForDeploymentPostprocessing = 1; 51 | }; 52 | /* End PBXCopyFilesBuildPhase section */ 53 | 54 | /* Begin PBXFileReference section */ 55 | 2DB32B78220D933300D23E57 /* SlackMessageReaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackMessageReaction.swift; path = Sources/SlackMessageReaction.swift; sourceTree = SOURCE_ROOT; }; 56 | 7C92EB541E03F3C900F92267 /* TerminalCanvas.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TerminalCanvas.swift; path = Sources/TerminalCanvas.swift; sourceTree = SOURCE_ROOT; }; 57 | 7C92EB551E03F3C900F92267 /* TerminalDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TerminalDevice.swift; path = Sources/TerminalDevice.swift; sourceTree = SOURCE_ROOT; }; 58 | 7C92EB561E03F3C900F92267 /* TextLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TextLayout.swift; path = Sources/TextLayout.swift; sourceTree = SOURCE_ROOT; }; 59 | 7C92EB5A1E03F3ED00F92267 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Application.swift; path = Sources/Application.swift; sourceTree = SOURCE_ROOT; }; 60 | 7C92EB5B1E03F3ED00F92267 /* ChannelsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ChannelsListView.swift; path = Sources/ChannelsListView.swift; sourceTree = SOURCE_ROOT; }; 61 | 7C92EB5C1E03F3ED00F92267 /* CrashReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CrashReporter.swift; path = Sources/CrashReporter.swift; sourceTree = SOURCE_ROOT; }; 62 | 7C92EB5D1E03F3ED00F92267 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = Sources/main.swift; sourceTree = SOURCE_ROOT; }; 63 | 7C92EB5E1E03F3ED00F92267 /* MessagesListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessagesListView.swift; path = Sources/MessagesListView.swift; sourceTree = SOURCE_ROOT; }; 64 | 7C92EB5F1E03F3ED00F92267 /* R.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = R.swift; path = Sources/R.swift; sourceTree = SOURCE_ROOT; }; 65 | 7C92EB601E03F3ED00F92267 /* Server.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Server.swift; path = Sources/Server.swift; sourceTree = SOURCE_ROOT; }; 66 | 7C92EB611E03F3ED00F92267 /* SlackAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackAdapter.swift; path = Sources/SlackAdapter.swift; sourceTree = SOURCE_ROOT; }; 67 | 7C92EB621E03F3ED00F92267 /* SlackContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackContext.swift; path = Sources/SlackContext.swift; sourceTree = SOURCE_ROOT; }; 68 | 7C92EB631E03F3ED00F92267 /* UserInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UserInputView.swift; path = Sources/UserInputView.swift; sourceTree = SOURCE_ROOT; }; 69 | 7C92EB641E03F3ED00F92267 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Utils.swift; path = Sources/Utils.swift; sourceTree = SOURCE_ROOT; }; 70 | 7C92EB701E03F3FC00F92267 /* SlackChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackChannel.swift; path = Sources/SlackChannel.swift; sourceTree = SOURCE_ROOT; }; 71 | 7C92EB711E03F3FC00F92267 /* SlackEmojiDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackEmojiDecoder.swift; path = Sources/SlackEmojiDecoder.swift; sourceTree = SOURCE_ROOT; }; 72 | 7C92EB721E03F3FC00F92267 /* SlackEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackEvent.swift; path = Sources/SlackEvent.swift; sourceTree = SOURCE_ROOT; }; 73 | 7C92EB731E03F3FC00F92267 /* SlackGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackGroup.swift; path = Sources/SlackGroup.swift; sourceTree = SOURCE_ROOT; }; 74 | 7C92EB741E03F3FC00F92267 /* SlackIM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackIM.swift; path = Sources/SlackIM.swift; sourceTree = SOURCE_ROOT; }; 75 | 7C92EB751E03F3FC00F92267 /* SlackMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackMessage.swift; path = Sources/SlackMessage.swift; sourceTree = SOURCE_ROOT; }; 76 | 7C92EB761E03F3FC00F92267 /* SlackOAuth2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackOAuth2.swift; path = Sources/SlackOAuth2.swift; sourceTree = SOURCE_ROOT; }; 77 | 7C92EB771E03F3FC00F92267 /* SlackRealTimeClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackRealTimeClient.swift; path = Sources/SlackRealTimeClient.swift; sourceTree = SOURCE_ROOT; }; 78 | 7C92EB781E03F3FC00F92267 /* SlackTeam.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackTeam.swift; path = Sources/SlackTeam.swift; sourceTree = SOURCE_ROOT; }; 79 | 7C92EB791E03F3FC00F92267 /* SlackUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackUser.swift; path = Sources/SlackUser.swift; sourceTree = SOURCE_ROOT; }; 80 | 7C92EB7A1E03F3FC00F92267 /* SlackWebClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackWebClient.swift; path = Sources/SlackWebClient.swift; sourceTree = SOURCE_ROOT; }; 81 | 7C92EB7B1E03F3FC00F92267 /* TLSSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TLSSocket.swift; path = Sources/TLSSocket.swift; sourceTree = SOURCE_ROOT; }; 82 | 7C92EB7C1E03F3FC00F92267 /* WebSocketClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WebSocketClient.swift; path = Sources/WebSocketClient.swift; sourceTree = SOURCE_ROOT; }; 83 | 7C9FE5F01E18F9B900FB8166 /* Socket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Socket.swift; path = Sources/Socket.swift; sourceTree = SOURCE_ROOT; }; 84 | 7CBB81F21DC12E8B00DC0334 /* slash */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = slash; sourceTree = BUILT_PRODUCTS_DIR; }; 85 | D42C5DF61E156BEE008CDA3C /* URLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLSession.swift; path = Sources/URLSession.swift; sourceTree = SOURCE_ROOT; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFrameworksBuildPhase section */ 89 | 7CBB81EF1DC12E8B00DC0334 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | /* End PBXFrameworksBuildPhase section */ 97 | 98 | /* Begin PBXGroup section */ 99 | 7C4A722A1DE5CC91005A72EF /* Sources */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | D42C5DF51E156A2A008CDA3C /* Extensions */, 103 | 7C4A725F1DE5CCD4005A72EF /* App */, 104 | 7C4A725E1DE5CCB6005A72EF /* Slack */, 105 | 7C4A725D1DE5CCA3005A72EF /* Terminal */, 106 | ); 107 | name = Sources; 108 | path = ../Sources; 109 | sourceTree = ""; 110 | }; 111 | 7C4A725D1DE5CCA3005A72EF /* Terminal */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 7C92EB541E03F3C900F92267 /* TerminalCanvas.swift */, 115 | 7C92EB551E03F3C900F92267 /* TerminalDevice.swift */, 116 | 7C92EB561E03F3C900F92267 /* TextLayout.swift */, 117 | ); 118 | name = Terminal; 119 | sourceTree = ""; 120 | }; 121 | 7C4A725E1DE5CCB6005A72EF /* Slack */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 7C9FE5F01E18F9B900FB8166 /* Socket.swift */, 125 | 7C92EB701E03F3FC00F92267 /* SlackChannel.swift */, 126 | 7C92EB711E03F3FC00F92267 /* SlackEmojiDecoder.swift */, 127 | 7C92EB721E03F3FC00F92267 /* SlackEvent.swift */, 128 | 7C92EB731E03F3FC00F92267 /* SlackGroup.swift */, 129 | 7C92EB741E03F3FC00F92267 /* SlackIM.swift */, 130 | 7C92EB751E03F3FC00F92267 /* SlackMessage.swift */, 131 | 2DB32B78220D933300D23E57 /* SlackMessageReaction.swift */, 132 | 7C92EB761E03F3FC00F92267 /* SlackOAuth2.swift */, 133 | 7C92EB771E03F3FC00F92267 /* SlackRealTimeClient.swift */, 134 | 7C92EB781E03F3FC00F92267 /* SlackTeam.swift */, 135 | 7C92EB791E03F3FC00F92267 /* SlackUser.swift */, 136 | 7C92EB7A1E03F3FC00F92267 /* SlackWebClient.swift */, 137 | 7C92EB7B1E03F3FC00F92267 /* TLSSocket.swift */, 138 | 7C92EB7C1E03F3FC00F92267 /* WebSocketClient.swift */, 139 | ); 140 | name = Slack; 141 | sourceTree = ""; 142 | }; 143 | 7C4A725F1DE5CCD4005A72EF /* App */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 7C92EB5A1E03F3ED00F92267 /* Application.swift */, 147 | 7C92EB5B1E03F3ED00F92267 /* ChannelsListView.swift */, 148 | 7C92EB5C1E03F3ED00F92267 /* CrashReporter.swift */, 149 | 7C92EB5D1E03F3ED00F92267 /* main.swift */, 150 | 7C92EB5E1E03F3ED00F92267 /* MessagesListView.swift */, 151 | 7C92EB5F1E03F3ED00F92267 /* R.swift */, 152 | 7C92EB601E03F3ED00F92267 /* Server.swift */, 153 | 7C92EB611E03F3ED00F92267 /* SlackAdapter.swift */, 154 | 7C92EB621E03F3ED00F92267 /* SlackContext.swift */, 155 | 7C92EB631E03F3ED00F92267 /* UserInputView.swift */, 156 | 7C92EB641E03F3ED00F92267 /* Utils.swift */, 157 | ); 158 | name = App; 159 | sourceTree = ""; 160 | }; 161 | 7CBB81E91DC12E8B00DC0334 = { 162 | isa = PBXGroup; 163 | children = ( 164 | 7C4A722A1DE5CC91005A72EF /* Sources */, 165 | 7CBB81F31DC12E8B00DC0334 /* Products */, 166 | ); 167 | sourceTree = ""; 168 | }; 169 | 7CBB81F31DC12E8B00DC0334 /* Products */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 7CBB81F21DC12E8B00DC0334 /* slash */, 173 | ); 174 | name = Products; 175 | sourceTree = ""; 176 | }; 177 | D42C5DF51E156A2A008CDA3C /* Extensions */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | D42C5DF61E156BEE008CDA3C /* URLSession.swift */, 181 | ); 182 | name = Extensions; 183 | sourceTree = ""; 184 | }; 185 | /* End PBXGroup section */ 186 | 187 | /* Begin PBXNativeTarget section */ 188 | 7CBB81F11DC12E8B00DC0334 /* slash */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 7CBB81F91DC12E8B00DC0334 /* Build configuration list for PBXNativeTarget "slash" */; 191 | buildPhases = ( 192 | 7CBB81EE1DC12E8B00DC0334 /* Sources */, 193 | 7CBB81EF1DC12E8B00DC0334 /* Frameworks */, 194 | 7CBB81F01DC12E8B00DC0334 /* CopyFiles */, 195 | ); 196 | buildRules = ( 197 | ); 198 | dependencies = ( 199 | ); 200 | name = slash; 201 | productName = swiftslack; 202 | productReference = 7CBB81F21DC12E8B00DC0334 /* slash */; 203 | productType = "com.apple.product-type.tool"; 204 | }; 205 | /* End PBXNativeTarget section */ 206 | 207 | /* Begin PBXProject section */ 208 | 7CBB81EA1DC12E8B00DC0334 /* Project object */ = { 209 | isa = PBXProject; 210 | attributes = { 211 | LastSwiftUpdateCheck = 0800; 212 | LastUpgradeCheck = 1000; 213 | ORGANIZATIONNAME = kolakowski; 214 | TargetAttributes = { 215 | 7CBB81F11DC12E8B00DC0334 = { 216 | CreatedOnToolsVersion = 8.0; 217 | LastSwiftMigration = 0900; 218 | ProvisioningStyle = Automatic; 219 | }; 220 | }; 221 | }; 222 | buildConfigurationList = 7CBB81ED1DC12E8B00DC0334 /* Build configuration list for PBXProject "slash" */; 223 | compatibilityVersion = "Xcode 3.2"; 224 | developmentRegion = English; 225 | hasScannedForEncodings = 0; 226 | knownRegions = ( 227 | en, 228 | ); 229 | mainGroup = 7CBB81E91DC12E8B00DC0334; 230 | productRefGroup = 7CBB81F31DC12E8B00DC0334 /* Products */; 231 | projectDirPath = ""; 232 | projectRoot = ""; 233 | targets = ( 234 | 7CBB81F11DC12E8B00DC0334 /* slash */, 235 | ); 236 | }; 237 | /* End PBXProject section */ 238 | 239 | /* Begin PBXSourcesBuildPhase section */ 240 | 7CBB81EE1DC12E8B00DC0334 /* Sources */ = { 241 | isa = PBXSourcesBuildPhase; 242 | buildActionMask = 2147483647; 243 | files = ( 244 | 7C92EB801E03F3FC00F92267 /* SlackGroup.swift in Sources */, 245 | 7C92EB6B1E03F3ED00F92267 /* Server.swift in Sources */, 246 | 7C92EB6E1E03F3ED00F92267 /* UserInputView.swift in Sources */, 247 | 2DB32B79220D933300D23E57 /* SlackMessageReaction.swift in Sources */, 248 | 7C92EB691E03F3ED00F92267 /* MessagesListView.swift in Sources */, 249 | 7C92EB881E03F3FC00F92267 /* TLSSocket.swift in Sources */, 250 | 7C92EB6F1E03F3ED00F92267 /* Utils.swift in Sources */, 251 | 7C92EB871E03F3FC00F92267 /* SlackWebClient.swift in Sources */, 252 | 7C92EB811E03F3FC00F92267 /* SlackIM.swift in Sources */, 253 | 7C92EB7D1E03F3FC00F92267 /* SlackChannel.swift in Sources */, 254 | 7C92EB861E03F3FC00F92267 /* SlackUser.swift in Sources */, 255 | 7C92EB671E03F3ED00F92267 /* CrashReporter.swift in Sources */, 256 | 7C92EB821E03F3FC00F92267 /* SlackMessage.swift in Sources */, 257 | 7C92EB681E03F3ED00F92267 /* main.swift in Sources */, 258 | 7C92EB7F1E03F3FC00F92267 /* SlackEvent.swift in Sources */, 259 | 7C92EB891E03F3FC00F92267 /* WebSocketClient.swift in Sources */, 260 | 7C92EB6C1E03F3ED00F92267 /* SlackAdapter.swift in Sources */, 261 | 7C92EB6A1E03F3ED00F92267 /* R.swift in Sources */, 262 | 7C92EB661E03F3ED00F92267 /* ChannelsListView.swift in Sources */, 263 | 7C92EB7E1E03F3FC00F92267 /* SlackEmojiDecoder.swift in Sources */, 264 | 7C92EB651E03F3ED00F92267 /* Application.swift in Sources */, 265 | 7C92EB831E03F3FC00F92267 /* SlackOAuth2.swift in Sources */, 266 | 7C92EB6D1E03F3ED00F92267 /* SlackContext.swift in Sources */, 267 | D42C5DF71E156BEE008CDA3C /* URLSession.swift in Sources */, 268 | 7C92EB841E03F3FC00F92267 /* SlackRealTimeClient.swift in Sources */, 269 | 7C92EB851E03F3FC00F92267 /* SlackTeam.swift in Sources */, 270 | 7C9FE5F11E18F9B900FB8166 /* Socket.swift in Sources */, 271 | 7C92EB581E03F3C900F92267 /* TerminalDevice.swift in Sources */, 272 | 7C92EB571E03F3C900F92267 /* TerminalCanvas.swift in Sources */, 273 | 7C92EB591E03F3C900F92267 /* TextLayout.swift in Sources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | /* End PBXSourcesBuildPhase section */ 278 | 279 | /* Begin XCBuildConfiguration section */ 280 | 7CBB81F71DC12E8B00DC0334 /* Debug */ = { 281 | isa = XCBuildConfiguration; 282 | buildSettings = { 283 | ALWAYS_SEARCH_USER_PATHS = NO; 284 | CLANG_ANALYZER_NONNULL = YES; 285 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 286 | CLANG_CXX_LIBRARY = "libc++"; 287 | CLANG_ENABLE_MODULES = YES; 288 | CLANG_ENABLE_OBJC_ARC = YES; 289 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 290 | CLANG_WARN_BOOL_CONVERSION = YES; 291 | CLANG_WARN_COMMA = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 294 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 295 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 296 | CLANG_WARN_EMPTY_BODY = YES; 297 | CLANG_WARN_ENUM_CONVERSION = YES; 298 | CLANG_WARN_INFINITE_RECURSION = YES; 299 | CLANG_WARN_INT_CONVERSION = YES; 300 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 302 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 304 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 305 | CLANG_WARN_STRICT_PROTOTYPES = YES; 306 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 307 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 308 | CLANG_WARN_UNREACHABLE_CODE = YES; 309 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 310 | CODE_SIGN_IDENTITY = "-"; 311 | COPY_PHASE_STRIP = NO; 312 | DEBUG_INFORMATION_FORMAT = dwarf; 313 | ENABLE_STRICT_OBJC_MSGSEND = YES; 314 | ENABLE_TESTABILITY = YES; 315 | GCC_C_LANGUAGE_STANDARD = gnu99; 316 | GCC_DYNAMIC_NO_PIC = NO; 317 | GCC_NO_COMMON_BLOCKS = YES; 318 | GCC_OPTIMIZATION_LEVEL = 0; 319 | GCC_PREPROCESSOR_DEFINITIONS = ( 320 | "DEBUG=1", 321 | "$(inherited)", 322 | ); 323 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 324 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 325 | GCC_WARN_UNDECLARED_SELECTOR = YES; 326 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 327 | GCC_WARN_UNUSED_FUNCTION = YES; 328 | GCC_WARN_UNUSED_VARIABLE = YES; 329 | MACOSX_DEPLOYMENT_TARGET = 10.12; 330 | MTL_ENABLE_DEBUG_INFO = YES; 331 | ONLY_ACTIVE_ARCH = YES; 332 | SDKROOT = macosx; 333 | SWIFT_INCLUDE_PATHS = ""; 334 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 335 | SWIFT_VERSION = 4.2; 336 | }; 337 | name = Debug; 338 | }; 339 | 7CBB81F81DC12E8B00DC0334 /* Release */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ALWAYS_SEARCH_USER_PATHS = NO; 343 | CLANG_ANALYZER_NONNULL = YES; 344 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 345 | CLANG_CXX_LIBRARY = "libc++"; 346 | CLANG_ENABLE_MODULES = YES; 347 | CLANG_ENABLE_OBJC_ARC = YES; 348 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 349 | CLANG_WARN_BOOL_CONVERSION = YES; 350 | CLANG_WARN_COMMA = YES; 351 | CLANG_WARN_CONSTANT_CONVERSION = YES; 352 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 353 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 354 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 355 | CLANG_WARN_EMPTY_BODY = YES; 356 | CLANG_WARN_ENUM_CONVERSION = YES; 357 | CLANG_WARN_INFINITE_RECURSION = YES; 358 | CLANG_WARN_INT_CONVERSION = YES; 359 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 360 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 361 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 363 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 364 | CLANG_WARN_STRICT_PROTOTYPES = YES; 365 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 366 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 367 | CLANG_WARN_UNREACHABLE_CODE = YES; 368 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 369 | CODE_SIGN_IDENTITY = "-"; 370 | COPY_PHASE_STRIP = NO; 371 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 372 | ENABLE_NS_ASSERTIONS = NO; 373 | ENABLE_STRICT_OBJC_MSGSEND = YES; 374 | GCC_C_LANGUAGE_STANDARD = gnu99; 375 | GCC_NO_COMMON_BLOCKS = YES; 376 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 377 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 378 | GCC_WARN_UNDECLARED_SELECTOR = YES; 379 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 380 | GCC_WARN_UNUSED_FUNCTION = YES; 381 | GCC_WARN_UNUSED_VARIABLE = YES; 382 | MACOSX_DEPLOYMENT_TARGET = 10.12; 383 | MTL_ENABLE_DEBUG_INFO = NO; 384 | SDKROOT = macosx; 385 | SWIFT_INCLUDE_PATHS = ""; 386 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 387 | SWIFT_VERSION = 4.2; 388 | }; 389 | name = Release; 390 | }; 391 | 7CBB81FA1DC12E8B00DC0334 /* Debug */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | LD_RUNPATH_SEARCH_PATHS = "@executable_path"; 395 | MACOSX_DEPLOYMENT_TARGET = 10.11; 396 | PRODUCT_NAME = "$(TARGET_NAME)"; 397 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 398 | SWIFT_VERSION = 4.0; 399 | }; 400 | name = Debug; 401 | }; 402 | 7CBB81FB1DC12E8B00DC0334 /* Release */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | LD_RUNPATH_SEARCH_PATHS = "@executable_path"; 406 | MACOSX_DEPLOYMENT_TARGET = 10.11; 407 | PRODUCT_NAME = "$(TARGET_NAME)"; 408 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 409 | SWIFT_VERSION = 4.0; 410 | }; 411 | name = Release; 412 | }; 413 | /* End XCBuildConfiguration section */ 414 | 415 | /* Begin XCConfigurationList section */ 416 | 7CBB81ED1DC12E8B00DC0334 /* Build configuration list for PBXProject "slash" */ = { 417 | isa = XCConfigurationList; 418 | buildConfigurations = ( 419 | 7CBB81F71DC12E8B00DC0334 /* Debug */, 420 | 7CBB81F81DC12E8B00DC0334 /* Release */, 421 | ); 422 | defaultConfigurationIsVisible = 0; 423 | defaultConfigurationName = Release; 424 | }; 425 | 7CBB81F91DC12E8B00DC0334 /* Build configuration list for PBXNativeTarget "slash" */ = { 426 | isa = XCConfigurationList; 427 | buildConfigurations = ( 428 | 7CBB81FA1DC12E8B00DC0334 /* Debug */, 429 | 7CBB81FB1DC12E8B00DC0334 /* Release */, 430 | ); 431 | defaultConfigurationIsVisible = 0; 432 | defaultConfigurationName = Release; 433 | }; 434 | /* End XCConfigurationList section */ 435 | }; 436 | rootObject = 7CBB81EA1DC12E8B00DC0334 /* Project object */; 437 | } 438 | -------------------------------------------------------------------------------- /slash.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /slash.xcodeproj/xcshareddata/xcschemes/slash.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | --------------------------------------------------------------------------------