├── .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 |
2 |
3 | Slack terminal client.
4 |
5 |
6 |
7 | 
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 |
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 | 
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 |
--------------------------------------------------------------------------------