├── .gitignore ├── LICENSE ├── README.md ├── app ├── app.html ├── helpers │ ├── context_menu.js │ └── external_links.js ├── history.html ├── js │ ├── const.js │ ├── controllers │ │ ├── historyController.js │ │ ├── requestController.js │ │ └── responseController.js │ ├── directives │ │ ├── fileModel.js │ │ ├── modalShow.js │ │ └── statusCode.js │ ├── filters │ │ └── objLength.js │ ├── ling.js │ └── services │ │ ├── historyService.js │ │ └── requestService.js ├── package.json ├── request.html └── response.html ├── config ├── env_development.json ├── env_production.json └── env_test.json ├── e2e ├── ling.e2e.js └── utils.js ├── gulpfile.js ├── ling.png ├── ling.psd ├── package.json ├── resources ├── icons │ └── 512x512.png ├── osx │ ├── dmg-background.png │ ├── dmg-background@2x.png │ ├── dmg-icon.icns │ └── icon.icns └── windows │ ├── icon.ico │ └── setup-icon.ico ├── screenshot.png ├── src ├── app.js ├── background.js ├── env.js ├── helpers │ └── window.js ├── menu │ ├── dev_menu_template.js │ └── edit_menu_template.js └── stylesheets │ ├── imports │ ├── bootstrap-override.scss │ ├── history.scss │ ├── layout.scss │ ├── mixins.scss │ ├── request.scss │ ├── response.scss │ ├── utilities.scss │ └── variables.scss │ └── style.scss └── tasks ├── build_app.js ├── build_tests.js ├── bundle.js ├── start.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | Thumbs.db 4 | *.log 5 | *.autogenerated 6 | 7 | # ignore everything in 'app' folder what had been generated from 'src' folder 8 | /app/stylesheets 9 | /app/app.js 10 | /app/background.js 11 | /app/env.json 12 | /app/**/*.map 13 | /app/node_modules 14 | 15 | /dist 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Jakub Szwacz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ling 2 | 3 | 4 | 5 | 6 | > REST client built with [Electron](https://github.com/electron/electron) and AngularJS. 7 | 8 | ## Clone 9 | 10 | ``` 11 | $ git clone https://github.com/talhasch/ling 12 | $ cd ling 13 | ``` 14 | 15 | ## Install dependencies 16 | 17 | ``` 18 | $ npm install 19 | ``` 20 | 21 | ### Run 22 | 23 | ``` 24 | $ npm start 25 | ``` 26 | 27 | ### Package 28 | 29 | ``` 30 | $ npm run release 31 | ``` 32 | 33 | ### Screenshot 34 | 35 | 36 | 37 | ### Ling? 38 | 39 | [Ling](https://en.wikipedia.org/wiki/Ling_Ling_(giant_panda)) 40 | 41 | ## License 42 | 43 | The MIT License (MIT) © Talha Buğra Bulut 2016 -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ling 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/helpers/context_menu.js: -------------------------------------------------------------------------------- 1 | // This gives you default context menu (cut, copy, paste) 2 | // in all input fields and textareas across your app. 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | var remote = require('electron').remote; 8 | var Menu = remote.Menu; 9 | var MenuItem = remote.MenuItem; 10 | 11 | var isAnyTextSelected = function () { 12 | return window.getSelection().toString() !== ''; 13 | }; 14 | 15 | var cut = new MenuItem({ 16 | label: "Cut", 17 | click: function () { 18 | document.execCommand("cut"); 19 | } 20 | }); 21 | 22 | var copy = new MenuItem({ 23 | label: "Copy", 24 | click: function () { 25 | document.execCommand("copy"); 26 | } 27 | }); 28 | 29 | var paste = new MenuItem({ 30 | label: "Paste", 31 | click: function () { 32 | document.execCommand("paste"); 33 | } 34 | }); 35 | 36 | var normalMenu = new Menu(); 37 | normalMenu.append(copy); 38 | 39 | var textEditingMenu = new Menu(); 40 | textEditingMenu.append(cut); 41 | textEditingMenu.append(copy); 42 | textEditingMenu.append(paste); 43 | 44 | document.addEventListener('contextmenu', function (e) { 45 | switch (e.target.nodeName) { 46 | case 'TEXTAREA': 47 | case 'INPUT': 48 | e.preventDefault(); 49 | textEditingMenu.popup(remote.getCurrentWindow()); 50 | break; 51 | default: 52 | if (isAnyTextSelected()) { 53 | e.preventDefault(); 54 | normalMenu.popup(remote.getCurrentWindow()); 55 | } 56 | } 57 | }, false); 58 | 59 | }()); 60 | -------------------------------------------------------------------------------- /app/helpers/external_links.js: -------------------------------------------------------------------------------- 1 | // Convenient way for opening links in external browser, not in the app. 2 | // Useful especially if you have a lot of links to deal with. 3 | // 4 | // Usage: 5 | // 6 | // Every link with class ".js-external-link" will be opened in external browser. 7 | // google 8 | // 9 | // The same behaviour for many links can be achieved by adding 10 | // this class to any parent tag of an anchor tag. 11 | // 15 | 16 | (function () { 17 | 'use strict'; 18 | 19 | var shell = require('electron').shell; 20 | 21 | var supportExternalLinks = function (e) { 22 | var href; 23 | var isExternal = false; 24 | 25 | var checkDomElement = function (element) { 26 | if (element.nodeName === 'A') { 27 | href = element.getAttribute('href'); 28 | } 29 | if (element.classList.contains('js-external-link')) { 30 | isExternal = true; 31 | } 32 | if (href && isExternal) { 33 | shell.openExternal(href); 34 | e.preventDefault(); 35 | } else if (element.parentElement) { 36 | checkDomElement(element.parentElement); 37 | } 38 | }; 39 | 40 | checkDomElement(e.target); 41 | }; 42 | 43 | document.addEventListener('click', supportExternalLinks, false); 44 | }()); 45 | -------------------------------------------------------------------------------- /app/history.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | HISTORY 4 |
5 | 6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
methodurldurationstatus
empty
{{ row.method }}{{ row.url | limitTo: 50 }}{{ row.url.length > 50 ? '...' : '' }}{{ row.duration }} ms{{ row.status }}
32 |
33 |
-------------------------------------------------------------------------------- /app/js/const.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'LINK', 'UNLINK', 'OPTIONS']; 4 | const METHOD_GET = 'GET', METHOD_HEAD = 'HEAD', METHOD_POST = 'POST', METHOD_PUT = 'PUT', METHOD_DELETE = 'DELETE'; 5 | const METHOD_LINK = 'LINK', METHOD_UNLINK = 'UNLINK', METHOD_OPTIONS = 'OPTIONS'; 6 | 7 | const CONTENT_TYPES = [ 8 | 'application/json', 'application/xml', 'application/atom+xml', 'multipart/form-data', 9 | 'multipart/alternative', 'multipart/mixed', 'application/x-www-form-urlencoded', 10 | 'application/base64', 'application/octet-stream', 'text/plain', 'text/css', 'text/html', 11 | 'application/javascript'] 12 | 13 | const CONTENT_TYPE_FORM = 'application/x-www-form-urlencoded'; 14 | const CONTENT_TYPE_FILE = 'multipart/form-data'; -------------------------------------------------------------------------------- /app/js/controllers/historyController.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | window.app.controller('HistoryCtrl', ['$scope', '$rootScope', 'historyService', ($scope, $rootScope, historyService) => { 5 | 6 | const loadData = () => { 7 | historyService.getHistory((docs) => { 8 | $scope.data = docs; 9 | $scope.$apply(); 10 | }); 11 | } 12 | 13 | $scope.data = []; 14 | loadData(); 15 | 16 | $scope.$on('history', (event) => { 17 | loadData(); 18 | }); 19 | 20 | $scope.clearHistory = () => { 21 | historyService.clearHistory((numRemoved) => { 22 | loadData(); 23 | }) 24 | } 25 | 26 | $scope.use = (historyItem) => { 27 | $rootScope.$broadcast('clear'); 28 | $rootScope.$broadcast('useHistory', historyItem); 29 | } 30 | }]); 31 | })(); -------------------------------------------------------------------------------- /app/js/controllers/requestController.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict' 3 | 4 | window.app.controller('RequestCtrl', ['$scope', '$rootScope', '$timeout', 'requestService', 'historyService', ($scope, $rootScope, $timeout, requestService, historyService) => { 5 | 6 | const contentTypeSelectionShouldVisible = () => { 7 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) { 8 | return false; 9 | } 10 | return true; 11 | } 12 | 13 | const payloadFormShouldEnable = () => { 14 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) { 15 | return false; 16 | } 17 | 18 | return true; 19 | } 20 | 21 | const paramsFormShouldEnable = () => { 22 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) { 23 | return false; 24 | } 25 | let curContentTypeHeader = $scope.headers['Content-Type']; 26 | if (curContentTypeHeader == CONTENT_TYPE_FORM || curContentTypeHeader == CONTENT_TYPE_FILE) { 27 | return true; 28 | } 29 | 30 | return false; 31 | } 32 | 33 | const filesFormShouldEnable = () => { 34 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) { 35 | return false; 36 | } 37 | let curContentTypeHeader = $scope.headers['Content-Type']; 38 | if (curContentTypeHeader == CONTENT_TYPE_FILE) { 39 | return true; 40 | } 41 | 42 | return false; 43 | } 44 | 45 | const payloadFromParams = () => { 46 | let payload = ''; 47 | for (let i = 0; i < $scope.params.length; i += 1) { 48 | let paramName = encodeURIComponent($scope.params[i]['name']); 49 | let paramValue = encodeURIComponent($scope.params[i]['value']); 50 | payload += paramName + '=' + paramValue + '&'; 51 | } 52 | return payload.replace(/&+$/, ''); // remove last & 53 | } 54 | 55 | const paramsFromPayload = (payload) => { 56 | let params = []; 57 | let _payload = payload.split('&'); 58 | for (let i = 0; i < _payload.length; i++) { 59 | let element = _payload[i]; 60 | let _param = element.split('='); 61 | let paramName = decodeURIComponent(_param[0]); 62 | let paramValue = _param[1] ? decodeURIComponent(_param[1]) : ''; 63 | params.push({ 'name': paramName, 'value': paramValue }); 64 | } 65 | 66 | return params; 67 | } 68 | 69 | // URL 70 | $scope.url = ''; 71 | 72 | // METHOD 73 | $scope.methods = METHODS; 74 | $scope.selectedMethod = METHOD_GET; 75 | 76 | // TABS 77 | $scope.selectedTab = 'headers'; 78 | $scope.selectTab = (tab) => { 79 | $scope.selectedTab = tab; 80 | 81 | if (tab == 'params') { 82 | if ($scope.payload != '') { 83 | $scope.params = paramsFromPayload($scope.payload); 84 | } 85 | 86 | } 87 | } 88 | 89 | // HEADERS 90 | $scope.headers = {}; 91 | 92 | $scope.newHeaderName = ''; 93 | $scope.newHeaderValue = ''; 94 | 95 | $scope.resetHeadersForm = () => { 96 | $scope.headersForm.$setPristine(); 97 | $scope.newHeaderName = ''; 98 | $scope.newHeaderValue = ''; 99 | } 100 | 101 | $scope.submitHeadersForm = (isValid) => { 102 | if (isValid) { 103 | let headerName = $scope.newHeaderName; 104 | let headerValue = $scope.newHeaderValue; 105 | 106 | $scope.headers[headerName] = headerValue; 107 | $scope.resetHeadersForm(); 108 | } 109 | } 110 | 111 | $scope.editHeader = (headerName, headerValue) => { 112 | $scope.resetHeadersForm(); 113 | 114 | $scope.newHeaderName = headerName; 115 | $scope.newHeaderValue = headerValue; 116 | 117 | delete $scope.headers[headerName] 118 | } 119 | 120 | $scope.removeHeader = (headerName) => { 121 | delete $scope.headers[headerName]; 122 | 123 | if (headerName == 'Authorization') { 124 | $scope.authUser = ''; 125 | $scope.authPass = ''; 126 | } 127 | } 128 | 129 | // CONTENT TYPE HEADER 130 | $scope.contentTypes = CONTENT_TYPES; 131 | 132 | $scope.contentTypeSelectionShouldVisible = () => { 133 | return contentTypeSelectionShouldVisible(); 134 | } 135 | 136 | $scope.contentTypeSelectionChanged = () => { 137 | if (!$scope.headers['Content-Type']) { 138 | delete $scope.headers['Content-Type']; 139 | } 140 | } 141 | 142 | // PAYLOAD 143 | $scope.payload = ''; 144 | 145 | $scope.payloadFormShouldEnable = () => { 146 | return payloadFormShouldEnable(); 147 | } 148 | 149 | $scope.$watchCollection('params', () => { 150 | $scope.payload = payloadFromParams(); 151 | }); 152 | 153 | $scope.switchMethodForPayload = () => { 154 | $scope.selectedMethod = METHOD_POST; 155 | } 156 | 157 | // PARAMS (data) 158 | $scope.params = []; 159 | 160 | $scope.addParam = (paramName, paramValue) => { 161 | $scope.params.push({ 'name': paramName, 'value': paramValue }); 162 | } 163 | 164 | $scope.removeParam = (param) => { 165 | let index = $scope.params.indexOf(param); 166 | $scope.params.splice(index, 1); 167 | } 168 | 169 | $scope.newParamName = ''; 170 | $scope.newParamValue = ''; 171 | 172 | $scope.resetParamsForm = () => { 173 | $scope.paramsForm.$setPristine(); 174 | $scope.newParamName = ''; 175 | $scope.newParamValue = ''; 176 | } 177 | 178 | $scope.submitParamsForm = (isValid) => { 179 | if (isValid) { 180 | $scope.addParam($scope.newParamName, $scope.newParamValue) 181 | $scope.resetParamsForm(); 182 | } 183 | } 184 | 185 | $scope.editParam = (param) => { 186 | $scope.resetParamsForm(); 187 | 188 | $scope.newParamName = param.name; 189 | $scope.newParamValue = param.value; 190 | 191 | $scope.removeParam(param); 192 | } 193 | 194 | $scope.paramsFormShouldEnable = () => { 195 | return paramsFormShouldEnable(); 196 | } 197 | 198 | $scope.switchContentTypeForParamsForm = () => { 199 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) { 200 | $scope.selectedMethod = METHOD_POST; 201 | } 202 | 203 | $scope.headers['Content-Type'] = CONTENT_TYPE_FORM; 204 | } 205 | 206 | // FILES 207 | $scope.files = []; 208 | 209 | $scope.addFile = (fileName, fileValue, fileLabel) => { 210 | $scope.files.push({ 'name': fileName, 'value': fileValue, 'label': fileLabel }); 211 | } 212 | 213 | $scope.removeFile = (file) => { 214 | let index = $scope.files.indexOf(file); 215 | $scope.files.splice(index, 1); 216 | } 217 | 218 | $scope.newFileName = ''; 219 | $scope.newFileValue = ''; 220 | 221 | $scope.resetFilesForm = () => { 222 | $scope.filesForm.$setPristine(); 223 | $scope.newFileName = ''; 224 | $scope.newFileValue = ''; 225 | } 226 | 227 | $scope.submitFilesForm = () => { 228 | $scope.filesForm.$setValidity('required', true); 229 | if (!$scope.newFileName || !$scope.newFileValue) { 230 | $scope.filesForm.$setValidity('required', false); 231 | return; 232 | } 233 | 234 | $scope.addFile($scope.newFileName, $scope.newFileValue[0], $scope.newFileValue[0].name); 235 | $scope.resetFilesForm(); 236 | } 237 | 238 | $scope.filesFormShouldEnable = () => { 239 | return filesFormShouldEnable(); 240 | } 241 | 242 | $scope.switchContentTypeForFilesForm = () => { 243 | if ($scope.selectedMethod == METHOD_GET || $scope.selectedMethod == METHOD_HEAD) { 244 | $scope.selectedMethod = METHOD_POST; 245 | } 246 | 247 | $scope.headers['Content-Type'] = CONTENT_TYPE_FILE; 248 | } 249 | 250 | // AUTH 251 | $scope.authUser = ''; 252 | $scope.authPass = ''; 253 | 254 | const watchAuth = () => { 255 | if ($scope.authUser || $scope.authPass) { 256 | $scope.headers['Authorization'] = 'Basic ' + btoa($scope.authUser + ':' + $scope.authPass); 257 | } else { 258 | delete $scope.headers['Authorization']; 259 | } 260 | } 261 | 262 | let watchAuthT = null; 263 | $scope.$watchGroup(['authUser', 'authPass'], () => { 264 | if (watchAuthT != null) { 265 | $timeout.cancel(watchAuthT); 266 | watchAuthT = null; 267 | } 268 | 269 | watchAuthT = $timeout(() => { watchAuth() }, 300); 270 | }); 271 | 272 | let request = null; 273 | 274 | $scope.showProgressDialog = false; 275 | 276 | $scope.run = (isValid) => { 277 | 278 | if (isValid) { 279 | let url = angular.copy($scope.url); 280 | let method = angular.copy($scope.selectedMethod); 281 | let headers = angular.copy($scope.headers); 282 | let _data = payloadFormShouldEnable() ? angular.copy($scope.payload) : ''; 283 | let files = $scope.files; 284 | 285 | if (headers['Content-Type'] == CONTENT_TYPE_FILE) { 286 | headers['Content-Type'] = undefined; 287 | 288 | let params = paramsFromPayload(_data); 289 | 290 | _data = new FormData(); 291 | 292 | for (let i = 0; i < params.length; i++) { 293 | let param = params[i]; 294 | _data.append(param.name, param.value); 295 | } 296 | 297 | for (let i = 0; i < files.length; i++) { 298 | let file = files[i]; 299 | _data.append(file.name, file.value); 300 | } 301 | } 302 | 303 | $scope.showProgressDialog = true; 304 | 305 | $rootScope.$broadcast('beforeRequest'); 306 | let startDate = new Date(); 307 | request = requestService.makeRequest(url, method, headers, _data, files); 308 | request.promise.then((response) => { 309 | httpCallback(response, startDate); 310 | }, (reason) => { 311 | $rootScope.$broadcast('requestError'); 312 | 313 | if (reason instanceof Error) { 314 | alert(reason.message); 315 | } 316 | 317 | if (reason instanceof Object) { 318 | if (reason.status != undefined) { 319 | if (reason.status == -1) { 320 | // cancelled 321 | return; 322 | } 323 | 324 | httpCallback(reason, startDate); 325 | } 326 | } 327 | 328 | }).finally(() => { 329 | $scope.showProgressDialog = false; 330 | request = null; 331 | }); 332 | } 333 | } 334 | 335 | const httpCallback = (responseObj, startDate) => { 336 | let endDate = new Date(); 337 | let duration = endDate - startDate; 338 | 339 | let responseHeaders = responseObj.headers(); 340 | let responseBody = responseObj.data; 341 | let responseStatusCode = responseObj.status; 342 | $rootScope.$broadcast('response', responseHeaders, responseBody, responseStatusCode); 343 | 344 | historyService.addToHistory( 345 | { 346 | 'url': $scope.url, 347 | 'method': $scope.selectedMethod, 348 | 'headers': $scope.headers, 349 | 'payload': $scope.payload, 350 | 'authUser': $scope.authUser, 351 | 'authPass': $scope.authPass, 352 | 'status': responseStatusCode, 353 | 'duration': duration, 354 | 'created': new Date() 355 | }, (doc) => { 356 | $rootScope.$broadcast('history'); 357 | }); 358 | } 359 | 360 | $scope.cancelRequest = () => { 361 | request.cancel('User cancelled'); 362 | } 363 | 364 | $scope.clear = () => { 365 | $rootScope.$broadcast('clear'); 366 | } 367 | 368 | $scope.$on('clear', (event) => { 369 | 370 | $scope.url = ''; 371 | $scope.selectedMethod = METHOD_GET; 372 | $scope.selectedTab = 'headers'; 373 | $scope.headers = {}; 374 | $scope.payload = ''; 375 | $scope.params = []; 376 | $scope.files = []; 377 | $scope.authUser = ''; 378 | $scope.authPass = ''; 379 | 380 | $scope.runForm.$setPristine(); 381 | $scope.headersForm.$setPristine(); 382 | $scope.paramsForm.$setPristine(); 383 | }); 384 | 385 | $scope.$on('useHistory', (event, historyItem) => { 386 | $scope.url = historyItem.url; 387 | $scope.selectedMethod = historyItem.method; 388 | $scope.headers = historyItem.headers; 389 | $scope.payload = historyItem.payload; 390 | if (paramsFormShouldEnable) { 391 | $scope.params = paramsFromPayload($scope.payload); 392 | } 393 | $scope.authUser = historyItem.authUser; 394 | $scope.authPass = historyItem.authPass; 395 | }); 396 | }]); 397 | })(); -------------------------------------------------------------------------------- /app/js/controllers/responseController.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | window.app.controller('ResponseCtrl', ['$scope', ($scope) => { 5 | 6 | const isBinaryContentType = function (contentType) { 7 | let p = contentType.match(/([^\/]+)\/([^;]+)/), 8 | type = p && p[1], 9 | subtype = p && p[2]; 10 | 11 | if (type === 'text') { 12 | return false; 13 | } 14 | else if (type === 'application' && (subtype == 'javascript' || subtype == 'json' || subtype == 'xml')) { 15 | return false; 16 | } 17 | return true; 18 | } 19 | 20 | const isJsonContentType = function (contentType) { 21 | let p = contentType.match(/([^\/]+)\/([^;]+)/), 22 | type = p && p[1], 23 | subtype = p && p[2]; 24 | 25 | if (subtype == 'json') { 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | 32 | $scope.selectedTab = 'headers'; 33 | $scope.selectTab = (tab) => { 34 | $scope.selectedTab = tab; 35 | } 36 | 37 | $scope.reset = () => { 38 | $scope.headers = {}; 39 | $scope.body = null; 40 | $scope.statusCode = null; 41 | $scope.isBinary = false; 42 | $scope.isJson = false; 43 | $scope.formatted = false; 44 | $scope.flag = false; 45 | } 46 | 47 | $scope.format = () => { 48 | try { 49 | let obj = JSON.parse($scope.body); 50 | $scope.body = JSON.stringify(obj, null, 4); 51 | $scope.formatted = true; 52 | } catch (e) { 53 | return; 54 | } 55 | } 56 | 57 | $scope.reset(); 58 | 59 | $scope.$on("response", (event, headers, body, statusCode) => { 60 | $scope.headers = headers; 61 | $scope.statusCode = statusCode; 62 | 63 | let contentType = headers['content-type']; 64 | 65 | if(contentType!==undefined){ 66 | $scope.isBinary = isBinaryContentType(headers['content-type']); 67 | } else { 68 | $scope.isBinary = false; 69 | } 70 | 71 | if (!$scope.isBinary) { 72 | $scope.body = body; 73 | } 74 | 75 | if(contentType !== undefined){ 76 | $scope.isJson = isJsonContentType(headers['content-type']); 77 | } else { 78 | $scope.isJson = false; 79 | } 80 | 81 | $scope.flag = true; 82 | }); 83 | 84 | $scope.$on('beforeRequest', (event) => { 85 | $scope.reset(); 86 | }); 87 | 88 | $scope.$on('requestError', (event) => { 89 | $scope.reset(); 90 | }); 91 | 92 | $scope.$on('clear', (event) => { 93 | $scope.reset(); 94 | }); 95 | 96 | $scope.$on('xhrDone', (event, xhr) => { 97 | $scope.responseUrl = xhr.responseURL; 98 | }); 99 | }]); 100 | })(); -------------------------------------------------------------------------------- /app/js/directives/fileModel.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | window.app.directive('fileModel', [() => { 5 | return { 6 | scope: { 7 | fileModel: '=' 8 | }, 9 | link: (scope, element, attributes) => { 10 | element.bind('change', (changeEvent) => { 11 | scope.fileModel = changeEvent.target.files; 12 | }); 13 | } 14 | } 15 | }]); 16 | })(); -------------------------------------------------------------------------------- /app/js/directives/modalShow.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | window.app.directive('modalShow', () => { 5 | return { 6 | restrict: 'A', 7 | scope: { 8 | modalVisible: '=' 9 | }, 10 | link: (scope, element, attrs) => { 11 | 12 | //Hide or show the modal 13 | scope.showModal = (visible) => { 14 | if (visible) { 15 | element.modal('show'); 16 | } 17 | else { 18 | element.modal('hide'); 19 | } 20 | } 21 | 22 | //Check to see if the modal-visible attribute exists 23 | if (!attrs.modalVisible) { 24 | //The attribute isn't defined, show the modal by default 25 | scope.showModal(true); 26 | } 27 | else { 28 | //Watch for changes to the modal-visible attribute 29 | scope.$watch('modalVisible', (newValue, oldValue) => { 30 | scope.showModal(newValue); 31 | }); 32 | 33 | //Update the visible value when the dialog is closed through UI actions (Ok, cancel, etc.) 34 | element.bind('hide.bs.modal', () => { 35 | scope.modalVisible = false; 36 | if (!scope.$$phase && !scope.$root.$$phase) 37 | scope.$apply(); 38 | }); 39 | } 40 | } 41 | }; 42 | }); 43 | })(); -------------------------------------------------------------------------------- /app/js/directives/statusCode.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | // from https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 5 | const statusCodes = { 6 | // 1xx Informational 7 | '100': { 'msg': 'Continue', 'cls': 'info' }, 8 | '101': { 'msg': 'Switching Protocols', 'cls': 'info' }, 9 | '102': { 'msg': 'Processing', 'cls': 'info' }, 10 | 11 | // 2xx Success 12 | '200': { 'msg': 'OK', 'cls': 'success' }, 13 | '201': { 'msg': 'Created', 'cls': 'success' }, 14 | '202': { 'msg': 'Accepted', 'cls': 'success' }, 15 | '203': { 'msg': 'Non-Authoritative Information', 'cls': 'success' }, 16 | '204': { 'msg': 'No Content', 'cls': 'success' }, 17 | '205': { 'msg': 'Reset Content', 'cls': 'success' }, 18 | '206': { 'msg': 'Partial Content', 'cls': 'success' }, 19 | '207': { 'msg': 'Multi-Status', 'cls': 'success' }, 20 | '208': { 'msg': 'Already Reported', 'cls': 'success' }, 21 | '226': { 'msg': 'IM Used', 'cls': 'success' }, 22 | 23 | // 3xx Redirection 24 | '300': { 'msg': 'Multiple Choices', 'cls': 'redir' }, 25 | '301': { 'msg': 'Moved Permanently', 'cls': 'redir' }, 26 | '302': { 'msg': 'Found', 'cls': 'redir' }, 27 | '303': { 'msg': 'See Other', 'cls': 'redir' }, 28 | '304': { 'msg': 'Not Modified', 'cls': 'redir' }, 29 | '305': { 'msg': 'Use Proxy', 'cls': 'redir' }, 30 | '306': { 'msg': 'Switch Proxy', 'cls': 'redir' }, 31 | '307': { 'msg': 'Temporary Redirect', 'cls': 'redir' }, 32 | '308': { 'msg': 'Permanent Redirect', 'cls': 'redir' }, 33 | 34 | // 4xx Client Error 35 | '400': { 'msg': 'Bad Request', 'cls': 'client-err' }, 36 | '401': { 'msg': 'Unauthorized', 'cls': 'client-err' }, 37 | '402': { 'msg': 'Payment Required', 'cls': 'client-err' }, 38 | '403': { 'msg': 'Forbidden', 'cls': 'client-err' }, 39 | '404': { 'msg': 'Not Found', 'cls': 'client-err' }, 40 | '405': { 'msg': 'Method Not Allowed', 'cls': 'client-err' }, 41 | '406': { 'msg': 'Not Acceptable', 'cls': 'client-err' }, 42 | '407': { 'msg': 'Proxy Authentication Required', 'cls': 'client-err' }, 43 | '408': { 'msg': 'Request Timeout', 'cls': 'client-err' }, 44 | '409': { 'msg': 'Conflict', 'cls': 'client-err' }, 45 | '410': { 'msg': 'Gone', 'cls': 'client-err' }, 46 | '411': { 'msg': 'Length Required', 'cls': 'client-err' }, 47 | '412': { 'msg': 'Precondition Failed', 'cls': 'client-err' }, 48 | '413': { 'msg': 'Payload Too Large', 'cls': 'client-err' }, 49 | '414': { 'msg': 'URI Too Long', 'cls': 'client-err' }, 50 | '415': { 'msg': 'Unsupported Media Type', 'cls': 'client-err' }, 51 | '416': { 'msg': 'Range Not Satisfiable', 'cls': 'client-err' }, 52 | '417': { 'msg': 'Expectation Failed', 'cls': 'client-err' }, 53 | '418': { 'msg': "I'm a teapot", 'cls': 'client-err' }, 54 | '421': { 'msg': 'Misdirected Request', 'cls': 'client-err' }, 55 | '422': { 'msg': 'Unprocessable Entity', 'cls': 'client-err' }, 56 | '423': { 'msg': 'Locked', 'cls': 'client-err' }, 57 | '424': { 'msg': 'Failed Dependency', 'cls': 'client-err' }, 58 | '426': { 'msg': 'Upgrade Required', 'cls': 'client-err' }, 59 | '428': { 'msg': 'Precondition Required', 'cls': 'client-err' }, 60 | '429': { 'msg': 'Too Many Requests', 'cls': 'client-err' }, 61 | '431': { 'msg': 'Request Header Fields Too Large', 'cls': 'client-err' }, 62 | '451': { 'msg': 'Unavailable For Legal Reasons', 'cls': 'client-err' }, 63 | 64 | // 5xx Server Error 65 | '500': { 'msg': 'Internal Server Error', 'cls': 'server-err' }, 66 | '501': { 'msg': 'Not Implemented', 'cls': 'server-err' }, 67 | '502': { 'msg': 'Bad Gateway', 'cls': 'server-err' }, 68 | '503': { 'msg': 'Service Unavailable', 'cls': 'server-err' }, 69 | '504': { 'msg': 'Gateway Timeout', 'cls': 'server-err' }, 70 | '505': { 'msg': 'HTTP Version Not Supported', 'cls': 'server-err' }, 71 | '506': { 'msg': 'Variant Also Negotiates', 'cls': 'server-err' }, 72 | '507': { 'msg': 'Insufficient Storage', 'cls': 'server-err' }, 73 | '508': { 'msg': 'Loop Detected', 'cls': 'server-err' }, 74 | '510': { 'msg': 'Not Extended', 'cls': 'server-err' }, 75 | '511': { 'msg': 'Network Authentication Required', 'cls': 'server-err' }, 76 | 77 | // Internet Information Services 78 | '440': { 'msg': 'Login Timeout', 'cls': 'iis' }, 79 | '449': { 'msg': 'Retry With', 'cls': 'iis' }, 80 | 81 | // nginx 82 | '444': { 'msg': 'No Response', 'cls': 'nginx' }, 83 | '495': { 'msg': 'SSL Certificate Error', 'cls': 'nginx' }, 84 | '496': { 'msg': 'SSL Certificate Required', 'cls': 'nginx' }, 85 | '497': { 'msg': 'HTTP Request Sent to HTTPS Port', 'cls': 'nginx' }, 86 | '499': { 'msg': 'Client Closed Request', 'cls': 'nginx' }, 87 | 88 | // CloudFlare 89 | '520': { 'msg': 'Unknown Error', 'cls': 'cloudflare' }, 90 | '521': { 'msg': 'Web Server Is Down', 'cls': 'cloudflare' }, 91 | '522': { 'msg': 'Connection Timed Out', 'cls': 'cloudflare' }, 92 | '523': { 'msg': 'Origin Is Unreachable', 'cls': 'cloudflare' }, 93 | '524': { 'msg': 'A Timeout Occurred', 'cls': 'cloudflare' }, 94 | '525': { 'msg': 'SSL Handshake Failed', 'cls': 'cloudflare' }, 95 | '526': { 'msg': 'Invalid SSL Certificate', 'cls': 'cloudflare' } 96 | }; 97 | 98 | app.directive("responseStatus", () => { 99 | return { 100 | scope: { 101 | code: '@' 102 | }, 103 | link: (scope, element, attrs) => { 104 | scope.$watch('code', (val) => { 105 | if (val) { 106 | scope.text = ''; 107 | scope.class = ''; 108 | if (statusCodes[val] != undefined) { 109 | scope.text = statusCodes[val].msg; 110 | scope.class = statusCodes[val].cls; 111 | } 112 | } 113 | }); 114 | }, 115 | template: '
{{code}} - {{text}}
' 116 | } 117 | }) 118 | })(); 119 | 120 | 121 | -------------------------------------------------------------------------------- /app/js/filters/objLength.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | window.app.filter('objLength', () => { 5 | return (object) => { 6 | return Object.keys(object).length; 7 | } 8 | }); 9 | 10 | })(); -------------------------------------------------------------------------------- /app/js/ling.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | var app = window.app = angular.module('LingApp', []); 5 | 6 | })(); -------------------------------------------------------------------------------- /app/js/services/historyService.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | const db = new Nedb({ 5 | filename: 'lingHistory.db', 6 | autoload: true 7 | }); 8 | 9 | window.app.factory('historyService', () => { 10 | const addToHistory = (doc, cb) => { 11 | db.insert(doc, (err, doc) => { 12 | if (typeof cb === 'function') { 13 | cb(doc); 14 | } 15 | }); 16 | } 17 | 18 | const getHistory = (cb) => { 19 | db.find({}).sort({ created: -1 }).limit(50).exec((err, docs) => { 20 | if (typeof cb === 'function') { 21 | cb(docs); 22 | } 23 | }); 24 | } 25 | 26 | const clearHistory = (cb) => { 27 | db.remove({}, { multi: true }, (err, numRemoved) => { 28 | if (typeof cb === 'function') { 29 | cb(numRemoved); 30 | } 31 | }); 32 | } 33 | 34 | return { 35 | addToHistory: addToHistory, 36 | getHistory: getHistory, 37 | clearHistory: clearHistory 38 | }; 39 | }); 40 | 41 | })(); -------------------------------------------------------------------------------- /app/js/services/requestService.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | window.app.factory('$xhrFactory', ['$rootScope', ($rootScope) => { 5 | return function createXhr(method, url) { 6 | const xhr = new window.XMLHttpRequest({ mozSystem: true }); 7 | xhr.onreadystatechange = () => { 8 | if (xhr.readyState === XMLHttpRequest.DONE) { 9 | if (xhr.responseURL.startsWith('file://')) { 10 | return; 11 | } 12 | $rootScope.$broadcast('xhrDone', xhr); 13 | } 14 | }; 15 | return xhr; 16 | }; 17 | }]); 18 | 19 | window.app.factory('requestService', ($http, $q) => { 20 | const makeRequest = (url, method, headers, data, files) => { 21 | let canceller = $q.defer(); 22 | 23 | let cancel = (reason) => { 24 | canceller.resolve(reason); 25 | }; 26 | 27 | let promise = $http({ 28 | url: url, 29 | method: method, 30 | cache: false, 31 | headers: headers, 32 | data: data, 33 | timeout: canceller.promise, 34 | transformResponse: null 35 | }); 36 | 37 | return { 38 | promise: promise, 39 | cancel: cancel 40 | }; 41 | } 42 | 43 | return { 44 | makeRequest: makeRequest 45 | }; 46 | }); 47 | 48 | })(); -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ling", 3 | "productName": "Ling", 4 | "description": "REST client", 5 | "version": "0.2.0", 6 | "author": "Talha Buğra Bulut ", 7 | "homepage": "https://github.com/talhasch/ling", 8 | "license": "MIT", 9 | "main": "background.js", 10 | "dependencies": { 11 | "fs-jetpack": "^0.9.0", 12 | "angular": "^1.5.8", 13 | "bootstrap": "^3.3.7", 14 | "font-awesome": "^4.6.3", 15 | "jquery": "^3.1.0", 16 | "nedb": "^1.8.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/request.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | REQUEST 4 |
5 | 6 |
7 |
8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 | 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 66 | 67 | 68 | 69 | 70 | 82 | 83 | 84 |
Empty
59 |
60 |
61 |
62 |
63 |
64 |
65 |
71 |
72 |
73 |
75 |
77 |
78 |
79 |
Name and value fields required.
80 |
81 |
85 |
86 |
87 |
88 | 89 |
90 | 91 |
92 |
93 |
94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 110 | 111 | 112 | 113 | 114 | 125 | 126 | 127 |
Empty
103 |
104 |
105 |
106 |
107 |
108 |
109 |
115 |
116 |
117 |
118 |
120 |
121 |
122 |
Name and value fields required.
123 |
124 |
128 |
129 |
130 |
131 | 132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 147 | 148 | 149 | 150 | 151 | 161 | 162 | 163 |
Empty
141 |
142 |
143 |
144 |
145 |
146 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
Name and file fields required.
159 |
160 |
164 | 165 |
166 |
167 |
168 | 169 |
170 |
171 | 172 |
173 |
174 |
175 |
176 | 191 |
-------------------------------------------------------------------------------- /app/response.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | RESPONSE 4 |
5 | 9 |
10 |
11 |
12 | {{ responseUrl }} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
NameValue
{{key}}{{ value }}
27 |
28 |
29 |
30 |
31 | 32 |
Unable to display binary data.
33 | 34 |
35 |
36 |
-------------------------------------------------------------------------------- /config/env_development.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "development" 3 | } 4 | -------------------------------------------------------------------------------- /config/env_production.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "production" 3 | } 4 | -------------------------------------------------------------------------------- /config/env_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test" 3 | } 4 | -------------------------------------------------------------------------------- /e2e/ling.e2e.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import testUtils from './utils'; 3 | 4 | describe('application launch', function () { 5 | 6 | beforeEach(testUtils.beforeEach); 7 | afterEach(testUtils.afterEach); 8 | 9 | it('Request section header', function () { 10 | return this.app.client.getText('.request .scope-header').then(function (text) { 11 | expect(text).to.equal('REQUEST'); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/utils.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import { Application } from 'spectron'; 3 | 4 | var beforeEach = function () { 5 | this.timeout(10000); 6 | this.app = new Application({ 7 | path: electron, 8 | args: ['app'], 9 | startTimeout: 10000, 10 | waitTimeout: 10000, 11 | }); 12 | return this.app.start(); 13 | }; 14 | 15 | var afterEach = function () { 16 | this.timeout(10000); 17 | if (this.app && this.app.isRunning()) { 18 | return this.app.stop(); 19 | } 20 | }; 21 | 22 | export default { 23 | beforeEach: beforeEach, 24 | afterEach: afterEach, 25 | }; 26 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./tasks/build_app'); 4 | require('./tasks/build_tests'); 5 | require('./tasks/start'); 6 | -------------------------------------------------------------------------------- /ling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/ling.png -------------------------------------------------------------------------------- /ling.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/ling.psd -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "win": { 4 | "target": [ 5 | "nsis" 6 | ], 7 | "icon": "resources/windows/icon.ico" 8 | }, 9 | "nsis": { 10 | "oneClick": true, 11 | "installerHeaderIcon": "resources/windows/setup-icon.ico" 12 | }, 13 | "mac": { 14 | "icon": "resources/osx/icon.icns" 15 | }, 16 | "dmg": { 17 | "icon": "resources/osx/dmg-icon.icns", 18 | "background": "resources/osx/dmg-background.png" 19 | } 20 | }, 21 | "directories": { 22 | "buildResources": "resources" 23 | }, 24 | "scripts": { 25 | "postinstall": "install-app-deps", 26 | "build": "gulp build", 27 | "prerelease": "gulp build --env=production", 28 | "release": "build --x64 --publish never", 29 | "start": "gulp start", 30 | "pretest": "gulp build-unit --env=test", 31 | "test": "electron-mocha app/specs.js.autogenerated --renderer --require source-map-support/register", 32 | "pree2e": "gulp build-e2e --env=test", 33 | "e2e": "mocha app/e2e.js.autogenerated --require source-map-support/register" 34 | }, 35 | "devDependencies": { 36 | "chai": "^3.5.0", 37 | "electron": "1.8.4", 38 | "electron-builder": "^5.12.1", 39 | "electron-mocha": "^3.0.0", 40 | "fs-jetpack": "^0.9.0", 41 | "gulp": "^3.9.0", 42 | "gulp-batch": "^1.0.5", 43 | "gulp-plumber": "^1.1.0", 44 | "gulp-sass": "^2.3.2", 45 | "gulp-util": "^3.0.6", 46 | "gulp-watch": "^4.3.5", 47 | "mocha": "^3.0.2", 48 | "rollup": "^0.34.7", 49 | "source-map-support": "^0.4.2", 50 | "spectron": "^3.3.0", 51 | "yargs": "^4.2.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/icons/512x512.png -------------------------------------------------------------------------------- /resources/osx/dmg-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/osx/dmg-background.png -------------------------------------------------------------------------------- /resources/osx/dmg-background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/osx/dmg-background@2x.png -------------------------------------------------------------------------------- /resources/osx/dmg-icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/osx/dmg-icon.icns -------------------------------------------------------------------------------- /resources/osx/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/osx/icon.icns -------------------------------------------------------------------------------- /resources/windows/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/windows/icon.ico -------------------------------------------------------------------------------- /resources/windows/setup-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/resources/windows/setup-icon.ico -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/screenshot.png -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // Here is the starting point for your application code. 2 | // All stuff below is just to show you how it works. You can delete all of it. 3 | 4 | // Use new ES6 modules syntax for everything. 5 | import os from 'os'; // native node.js module 6 | import { remote } from 'electron'; // native electron module 7 | const {ipcRenderer} = require('electron'); 8 | import jetpack from 'fs-jetpack'; // module loaded from npm 9 | import env from './env'; 10 | 11 | console.log('Loaded environment variables:', env); 12 | 13 | var app = remote.app; 14 | var appDir = jetpack.cwd(app.getAppPath()); 15 | 16 | // Holy crap! This is browser window with HTML and stuff, but I can read 17 | // here files like it is node.js! Welcome to Electron world :) 18 | console.log('The author of this app is:', appDir.read('package.json', 'json').author); 19 | 20 | document.addEventListener('DOMContentLoaded', function () { 21 | setTimeout(() => { 22 | document.body.style.visibility = 'visible'; 23 | }, 300); 24 | }); 25 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // This is main process of Electron, started as first thing when your 2 | // app starts. This script is running through entire life of your application. 3 | // It doesn't have any windows which you can see on screen, but we can open 4 | // window from here. 5 | 6 | import { app, Menu } from 'electron'; 7 | import { devMenuTemplate } from './menu/dev_menu_template'; 8 | import { editMenuTemplate } from './menu/edit_menu_template'; 9 | import createWindow from './helpers/window'; 10 | 11 | // Special module holding environment variables which you declared 12 | // in config/env_xxx.json file. 13 | import env from './env'; 14 | 15 | var mainWindow; 16 | 17 | var setApplicationMenu = function () { 18 | var menus = [editMenuTemplate]; 19 | if (env.name !== 'production') { 20 | menus.push(devMenuTemplate); 21 | } 22 | Menu.setApplicationMenu(Menu.buildFromTemplate(menus)); 23 | }; 24 | 25 | // Save userData in separate folders for each environment. 26 | // Thanks to this you can use production and development versions of the app 27 | // on same machine like those are two separate apps. 28 | if (env.name !== 'production') { 29 | var userDataPath = app.getPath('userData'); 30 | app.setPath('userData', userDataPath + ' (' + env.name + ')'); 31 | } 32 | 33 | app.on('ready', function () { 34 | setApplicationMenu(); 35 | 36 | var mainWindow = createWindow('main', { 37 | width: 1200, 38 | height: 700, 39 | minWidth: 1200, 40 | minHeight: 700 41 | }); 42 | 43 | mainWindow.loadURL('file://' + __dirname + '/app.html'); 44 | 45 | if (env.name === 'development') { 46 | mainWindow.openDevTools({detach: true, width: 200, height:200}); 47 | } 48 | }); 49 | 50 | app.on('window-all-closed', function () { 51 | app.quit(); 52 | }); 53 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | // Simple wrapper exposing environment variables to rest of the code. 2 | 3 | import jetpack from 'fs-jetpack'; 4 | 5 | // The variables have been written to `env.json` by the build process. 6 | var env = jetpack.cwd(__dirname).read('env.json', 'json'); 7 | 8 | export default env; 9 | -------------------------------------------------------------------------------- /src/helpers/window.js: -------------------------------------------------------------------------------- 1 | // This helper remembers the size and position of your windows (and restores 2 | // them in that place after app relaunch). 3 | // Can be used for more than one window, just construct many 4 | // instances of it and give each different name. 5 | 6 | import { app, BrowserWindow, screen } from 'electron'; 7 | import jetpack from 'fs-jetpack'; 8 | 9 | export default function (name, options) { 10 | 11 | var userDataDir = jetpack.cwd(app.getPath('userData')); 12 | var stateStoreFile = 'window-state-' + name +'.json'; 13 | var defaultSize = { 14 | width: options.width, 15 | height: options.height 16 | }; 17 | var state = {}; 18 | var win; 19 | 20 | var restore = function () { 21 | var restoredState = {}; 22 | try { 23 | restoredState = userDataDir.read(stateStoreFile, 'json'); 24 | } catch (err) { 25 | // For some reason json can't be read (might be corrupted). 26 | // No worries, we have defaults. 27 | } 28 | return Object.assign({}, defaultSize, restoredState); 29 | }; 30 | 31 | var getCurrentPosition = function () { 32 | var position = win.getPosition(); 33 | var size = win.getSize(); 34 | return { 35 | x: position[0], 36 | y: position[1], 37 | width: size[0], 38 | height: size[1] 39 | }; 40 | }; 41 | 42 | var windowWithinBounds = function (windowState, bounds) { 43 | return windowState.x >= bounds.x && 44 | windowState.y >= bounds.y && 45 | windowState.x + windowState.width <= bounds.x + bounds.width && 46 | windowState.y + windowState.height <= bounds.y + bounds.height; 47 | }; 48 | 49 | var resetToDefaults = function (windowState) { 50 | var bounds = screen.getPrimaryDisplay().bounds; 51 | return Object.assign({}, defaultSize, { 52 | x: (bounds.width - defaultSize.width) / 2, 53 | y: (bounds.height - defaultSize.height) / 2 54 | }); 55 | }; 56 | 57 | var ensureVisibleOnSomeDisplay = function (windowState) { 58 | var visible = screen.getAllDisplays().some(function (display) { 59 | return windowWithinBounds(windowState, display.bounds); 60 | }); 61 | if (!visible) { 62 | // Window is partially or fully not visible now. 63 | // Reset it to safe defaults. 64 | return resetToDefaults(windowState); 65 | } 66 | return windowState; 67 | }; 68 | 69 | var saveState = function () { 70 | if (!win.isMinimized() && !win.isMaximized()) { 71 | Object.assign(state, getCurrentPosition()); 72 | } 73 | userDataDir.write(stateStoreFile, state, { atomic: true }); 74 | }; 75 | 76 | state = ensureVisibleOnSomeDisplay(restore()); 77 | 78 | win = new BrowserWindow(Object.assign({}, options, state)); 79 | 80 | win.on('close', saveState); 81 | 82 | return win; 83 | } 84 | -------------------------------------------------------------------------------- /src/menu/dev_menu_template.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | export var devMenuTemplate = { 4 | label: 'Development', 5 | submenu: [{ 6 | label: 'Reload', 7 | accelerator: 'CmdOrCtrl+R', 8 | click: function () { 9 | BrowserWindow.getFocusedWindow().webContents.reloadIgnoringCache(); 10 | } 11 | },{ 12 | label: 'Toggle DevTools', 13 | accelerator: 'Alt+CmdOrCtrl+I', 14 | click: function () { 15 | BrowserWindow.getFocusedWindow().toggleDevTools(); 16 | } 17 | },{ 18 | label: 'Quit', 19 | accelerator: 'CmdOrCtrl+Q', 20 | click: function () { 21 | app.quit(); 22 | } 23 | }] 24 | }; 25 | -------------------------------------------------------------------------------- /src/menu/edit_menu_template.js: -------------------------------------------------------------------------------- 1 | const {shell} = require('electron'); 2 | 3 | export var editMenuTemplate = { 4 | label: 'Edit', 5 | submenu: [ 6 | { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" }, 7 | { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, 8 | { type: "separator" }, 9 | { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, 10 | { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, 11 | { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, 12 | { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }, 13 | { type: "separator" }, 14 | { label: "Github", click: () => { shell.openExternal('https://github.com/talhasch/ling') } } 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /src/stylesheets/imports/bootstrap-override.scss: -------------------------------------------------------------------------------- 1 | .btn{ 2 | &.btn-primary{ 3 | background-color: #3498db; 4 | border-color: #3498db; 5 | } 6 | &.btn-success{ 7 | background-color: #81b53e; 8 | border-color: #81b53e; 9 | } 10 | &.btn-warning{ 11 | background-color: #f0ad4e; 12 | border-color: #f0ad4e; 13 | } 14 | &.btn-inverse{ 15 | background-color: #3a444e; 16 | border-color: #3a444e; 17 | } 18 | &.btn-danger{ 19 | background-color: #e74c3c; 20 | border-color: #e74c3c; 21 | } 22 | } 23 | 24 | 25 | textarea:hover, 26 | input:hover, 27 | textarea:active, 28 | input:active, 29 | textarea:focus, 30 | input:focus, 31 | button:focus, 32 | button:active, 33 | button:hover 34 | { 35 | outline:0px !important; 36 | -webkit-appearance:none; 37 | } 38 | -------------------------------------------------------------------------------- /src/stylesheets/imports/history.scss: -------------------------------------------------------------------------------- 1 | .history{ 2 | position: absolute; 3 | right:0; 4 | bottom:0; 5 | width: 50%; 6 | height: 30%; 7 | padding: 55px 10px 10px 10px; 8 | box-sizing: border-box; 9 | 10 | .history-container{ 11 | height:100%; 12 | 13 | .history-table{ 14 | height:100%; 15 | overflow: auto; 16 | 17 | table{ 18 | tr{ 19 | .apply{ 20 | visibility: hidden; 21 | cursor: pointer; 22 | } 23 | th, td{ 24 | padding: 3px; 25 | font-size: 12px; 26 | } 27 | } 28 | 29 | tr:hover{ 30 | .apply{ 31 | visibility: visible; 32 | } 33 | } 34 | } 35 | } 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /src/stylesheets/imports/layout.scss: -------------------------------------------------------------------------------- 1 | html,body{ 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | body{ 7 | background: #ecf0f1; 8 | } 9 | 10 | .wrapper{ 11 | position: absolute; 12 | width: 100%; 13 | height: 100%; 14 | left: 0; 15 | top: 0; 16 | } -------------------------------------------------------------------------------- /src/stylesheets/imports/mixins.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/src/stylesheets/imports/mixins.scss -------------------------------------------------------------------------------- /src/stylesheets/imports/request.scss: -------------------------------------------------------------------------------- 1 | 2 | .request{ 3 | position: absolute; 4 | left:0; 5 | top:0; 6 | width: 50%; 7 | height: 100%; 8 | padding: 55px 10px 10px 10px; 9 | box-sizing: border-box; 10 | background-color: #ecf0f5; 11 | border-right: 3px solid #d2d6de; 12 | 13 | .request-container{ 14 | height:100%; 15 | 16 | /* 17 | row1 = url, run button 18 | row2 = method, content type 19 | row3 = tabs and tab contents (headers, raw payload, data, files, auth) 20 | */ 21 | 22 | .row1, .row2{ 23 | margin-bottom: 20px; 24 | } 25 | 26 | .row1{ 27 | form{ 28 | &.ng-submitted{ 29 | input{ 30 | &.ng-invalid{ 31 | border-color: red; 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | .row3{ 39 | /* 130px = .row1's height + .row2's height (approximate) */ 40 | height: calc(100% - 132px); 41 | 42 | .col{ 43 | height: 100%; 44 | } 45 | } 46 | 47 | .tabs, .tab-content{ 48 | width: 100%; 49 | margin-left: auto; 50 | margin-right: auto; 51 | } 52 | 53 | .tabs{ 54 | height:41px; 55 | 56 | .btn { 57 | background-color: #2c3b41; 58 | color:#fdfdfd; 59 | border-width: 0; 60 | border-radius: 0; 61 | color:#839ca8; 62 | font-size:14px; 63 | border-bottom: 3px solid #1e282c; 64 | 65 | &.active{ 66 | border-bottom: 3px solid #3c8dbc; 67 | color:#fff; 68 | } 69 | 70 | &:focus{ 71 | outline: none; 72 | } 73 | 74 | .label{ 75 | font-size: 12px; 76 | line-height: 100%; 77 | padding: 1px 2px; 78 | background-color: #2c3b41; 79 | color: #3c8dbc; 80 | visibility: hidden; 81 | margin-left: 2px; 82 | &.active{ 83 | visibility: visible; 84 | } 85 | } 86 | 87 | &.active{ 88 | .label{ 89 | background-color: #fff; 90 | } 91 | } 92 | } 93 | } 94 | 95 | .tab-content{ 96 | /* 38px = .tabs's height (approximate) */ 97 | height: calc(100% - 41px); 98 | padding: 10px; 99 | overflow: auto; 100 | background-color: #58656b; 101 | 102 | textarea{ 103 | height: 100%; 104 | resize: none; 105 | background: #f7f7f7; 106 | font-size: 16px; 107 | } 108 | 109 | .table{ 110 | 111 | &> thead{ 112 | &> tr{ 113 | &> th{ 114 | color: #ffffff; 115 | border-color: #374850; 116 | } 117 | } 118 | } 119 | 120 | &> tbody{ 121 | &> tr{ 122 | &> td{ 123 | border-top: 1px solid #374850; 124 | color: #839ca8; 125 | word-wrap: break-word; 126 | overflow-wrap: break-word; 127 | } 128 | } 129 | } 130 | 131 | &> tfoot{ 132 | 133 | &> tr{ 134 | &> td{ 135 | padding-top: 30px; 136 | border-top: 1px solid #374850; 137 | } 138 | } 139 | } 140 | } 141 | 142 | form{ 143 | 144 | &.ng-submitted{ 145 | input{ 146 | &.ng-invalid{ 147 | border-color: red; 148 | } 149 | } 150 | } 151 | 152 | .alert{ 153 | padding:5px; 154 | margin: 4px 0; 155 | } 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /src/stylesheets/imports/response.scss: -------------------------------------------------------------------------------- 1 | .response{ 2 | position: absolute; 3 | right:0; 4 | top:0; 5 | width: 50%; 6 | height: 70%; 7 | padding: 55px 10px 10px 10px; 8 | box-sizing: border-box; 9 | border-bottom: 3px solid #d2d6de; 10 | 11 | .response-container{ 12 | height:100%; 13 | 14 | .nav-tabs{ 15 | li{ 16 | &.active{ 17 | a{ 18 | font-weight: 600; 19 | background-color: #ecf0f5; 20 | } 21 | } 22 | } 23 | } 24 | 25 | .tab-content{ 26 | /* 42px = .nav-tabs's height */ 27 | height: calc(100% - 42px); 28 | padding: 10px; 29 | 30 | box-sizing: border-box; 31 | background-color: #ecf0f5; 32 | border-left: 1px solid #ccc; 33 | border-right: 1px solid #ccc; 34 | border-bottom: 1px solid #ccc; 35 | 36 | &.tab-content-headers{ 37 | .response-headers{ 38 | height: 100%; 39 | overflow: auto; 40 | 41 | .status-text{ 42 | font-size: 22px; 43 | margin-bottom: 10px; 44 | color: #333; 45 | 46 | &.success{ 47 | color: green; 48 | } 49 | 50 | &.client-err{ 51 | color: red; 52 | } 53 | 54 | &.server-err{ 55 | color: red; 56 | } 57 | } 58 | 59 | .response-url{ 60 | font-size: 16px; 61 | margin-bottom: 20px; 62 | max-width: 90%; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | text-overflow: ellipsis; 66 | display: block; 67 | } 68 | 69 | .table{ 70 | td{ 71 | font-family: monospace 72 | } 73 | } 74 | } 75 | } 76 | 77 | &.tab-content-body{ 78 | position: relative; 79 | 80 | .response-body{ 81 | height: 100%; 82 | 83 | textarea{ 84 | height: 100%; 85 | resize: none; 86 | background: #f7f7f7; 87 | font-size: 12px; 88 | } 89 | 90 | .btn-format{ 91 | position: absolute; 92 | right: 10px; 93 | top: 10px; 94 | } 95 | 96 | } 97 | 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/stylesheets/imports/utilities.scss: -------------------------------------------------------------------------------- 1 | 2 | .col-no-right-pad{ 3 | padding-right: 0; 4 | } 5 | 6 | .col-no-left-pad{ 7 | padding-left: 0; 8 | } 9 | 10 | .col-no-pad{ 11 | padding-left: 0; 12 | padding-right: 0 13 | } 14 | -------------------------------------------------------------------------------- /src/stylesheets/imports/variables.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talhasch/ling/2ef04a0b8bc059c5330aaab0d9ca6edce9cab2d6/src/stylesheets/imports/variables.scss -------------------------------------------------------------------------------- /src/stylesheets/style.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,300,700&subset=latin,latin-ext); 2 | 3 | body { 4 | font-family: 'Roboto', sans-serif; 5 | } 6 | 7 | 8 | // Core variables and mixins 9 | @import "imports/variables"; 10 | @import "imports/mixins"; 11 | 12 | // Core layouts 13 | @import "imports/layout"; 14 | @import "imports/utilities"; 15 | 16 | @import "imports/bootstrap-override.scss"; 17 | 18 | body{ 19 | visibility: hidden; 20 | } 21 | 22 | .scope-header{ 23 | position: absolute; 24 | left:0; 25 | top:0; 26 | right:0; 27 | background: #fff; 28 | color: #6e7882; 29 | box-sizing: border-box; 30 | height: 45px; 31 | padding: 10px 0 0 15px; 32 | font-size: 20px; 33 | 34 | &>.fa{ 35 | margin-right: 10px; 36 | } 37 | 38 | .buttons{ 39 | position: absolute; 40 | right: 10px; 41 | top: 5px; 42 | } 43 | } 44 | 45 | @import "imports/request.scss"; 46 | @import "imports/response.scss"; 47 | @import "imports/history.scss"; -------------------------------------------------------------------------------- /tasks/build_app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var sass = require('gulp-sass'); 5 | var watch = require('gulp-watch'); 6 | var batch = require('gulp-batch'); 7 | var plumber = require('gulp-plumber'); 8 | var jetpack = require('fs-jetpack'); 9 | var bundle = require('./bundle'); 10 | var utils = require('./utils'); 11 | 12 | var projectDir = jetpack; 13 | var srcDir = jetpack.cwd('./src'); 14 | var destDir = jetpack.cwd('./app'); 15 | 16 | gulp.task('bundle', function () { 17 | return Promise.all([ 18 | bundle(srcDir.path('background.js'), destDir.path('background.js')), 19 | bundle(srcDir.path('app.js'), destDir.path('app.js')), 20 | ]); 21 | }); 22 | 23 | gulp.task('sass', function () { 24 | return gulp.src(srcDir.path('stylesheets/style.scss')) 25 | .pipe(plumber()) 26 | .pipe(sass({outputStyle: 'compressed'})) 27 | .pipe(gulp.dest(destDir.path('stylesheets'))); 28 | }); 29 | 30 | gulp.task('environment', function () { 31 | var configFile = 'config/env_' + utils.getEnvName() + '.json'; 32 | projectDir.copy(configFile, destDir.path('env.json'), { overwrite: true }); 33 | }); 34 | 35 | gulp.task('watch', function () { 36 | var beepOnError = function (done) { 37 | return function (err) { 38 | if (err) { 39 | utils.beepSound(); 40 | } 41 | done(err); 42 | }; 43 | }; 44 | 45 | watch('src/**/*.js', batch(function (events, done) { 46 | gulp.start('bundle', beepOnError(done)); 47 | })); 48 | 49 | watch('src/**/*.scss', batch(function (events, done) { 50 | gulp.start('sass', beepOnError(done)); 51 | })); 52 | }); 53 | 54 | gulp.task('build', ['bundle', 'sass', 'environment']); 55 | -------------------------------------------------------------------------------- /tasks/build_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var jetpack = require('fs-jetpack'); 5 | var bundle = require('./bundle'); 6 | 7 | // Spec files are scattered through the whole project. Here we're searching 8 | // for them and generate one entry file which will run all the tests. 9 | var generateEntryFile = function (dir, destFileName, filePattern) { 10 | var fileBanner = "// This file is generated automatically.\n" 11 | + "// All modifications will be lost.\n"; 12 | 13 | return dir.findAsync('.', { matching: filePattern }) 14 | .then(function (specPaths) { 15 | var fileContent = specPaths.map(function (path) { 16 | return 'import "./' + path.replace(/\\/g, '/') + '";'; 17 | }).join('\n'); 18 | return dir.writeAsync(destFileName, fileBanner + fileContent); 19 | }) 20 | .then(function () { 21 | return dir.path(destFileName); 22 | }); 23 | }; 24 | 25 | gulp.task('build-unit', ['environment'], function () { 26 | var srcDir = jetpack.cwd('src'); 27 | var destDir = jetpack.cwd('app'); 28 | 29 | return generateEntryFile(srcDir, 'specs.js.autogenerated', '*.spec.js') 30 | .then(function (entryFilePath) { 31 | return bundle(entryFilePath, destDir.path('specs.js.autogenerated')); 32 | }); 33 | }); 34 | 35 | gulp.task('build-e2e', ['build'], function () { 36 | var srcDir = jetpack.cwd('e2e'); 37 | var destDir = jetpack.cwd('app'); 38 | 39 | return generateEntryFile(srcDir, 'e2e.js.autogenerated', '*.e2e.js') 40 | .then(function (entryFilePath) { 41 | return bundle(entryFilePath, destDir.path('e2e.js.autogenerated')); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tasks/bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var jetpack = require('fs-jetpack'); 5 | var rollup = require('rollup').rollup; 6 | 7 | var nodeBuiltInModules = ['assert', 'buffer', 'child_process', 'cluster', 8 | 'console', 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 9 | 'fs', 'http', 'https', 'module', 'net', 'os', 'path', 'process', 'punycode', 10 | 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 11 | 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib']; 12 | 13 | var electronBuiltInModules = ['electron']; 14 | 15 | var npmModulesUsedInApp = function () { 16 | var appManifest = require('../app/package.json'); 17 | return Object.keys(appManifest.dependencies); 18 | }; 19 | 20 | var generateExternalModulesList = function () { 21 | return [].concat(nodeBuiltInModules, electronBuiltInModules, npmModulesUsedInApp()); 22 | }; 23 | 24 | var cached = {}; 25 | 26 | module.exports = function (src, dest) { 27 | return rollup({ 28 | entry: src, 29 | external: generateExternalModulesList(), 30 | cache: cached[src], 31 | }) 32 | .then(function (bundle) { 33 | cached[src] = bundle; 34 | 35 | var jsFile = path.basename(dest); 36 | var result = bundle.generate({ 37 | format: 'cjs', 38 | sourceMap: true, 39 | sourceMapFile: jsFile, 40 | }); 41 | // Wrap code in self invoking function so the variables don't 42 | // pollute the global namespace. 43 | var isolatedCode = '(function () {' + result.code + '\n}());'; 44 | return Promise.all([ 45 | jetpack.writeAsync(dest, isolatedCode + '\n//# sourceMappingURL=' + jsFile + '.map'), 46 | jetpack.writeAsync(dest + '.map', result.map.toString()), 47 | ]); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /tasks/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var childProcess = require('child_process'); 4 | var electron = require('electron'); 5 | var gulp = require('gulp'); 6 | 7 | gulp.task('start', ['build', 'watch'], function () { 8 | childProcess.spawn(electron, ['./app'], { 9 | stdio: 'inherit' 10 | }) 11 | .on('close', function () { 12 | // User closed the app. Kill the host process. 13 | process.exit(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tasks/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var argv = require('yargs').argv; 4 | 5 | exports.getEnvName = function () { 6 | return argv.env || 'development'; 7 | }; 8 | 9 | exports.beepSound = function () { 10 | process.stdout.write('\u0007'); 11 | }; 12 | --------------------------------------------------------------------------------