├── .bowerrc
├── .eslintrc.json
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .travis.yml
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── build
├── background.png
└── icon.icns
├── build_env.sh
├── build_files
└── Info.plist
├── index.html
├── index2.html
├── main.js
├── package-lock.json
├── package.json
├── src
├── app
│ ├── app.js
│ ├── app2.js
│ ├── css
│ │ ├── app.css
│ │ ├── app2.css
│ │ ├── fonts.css
│ │ ├── fonts
│ │ │ ├── MaterialIcons.woff
│ │ │ ├── Roboto-Bold.woff
│ │ │ ├── Roboto-Italic.woff
│ │ │ ├── Roboto-Light.woff
│ │ │ ├── Roboto-Medium.woff
│ │ │ └── Roboto-Regular.woff
│ │ └── icons.css
│ ├── images
│ │ └── tray_icon.png
│ ├── js
│ │ ├── constants.js
│ │ ├── docs.js
│ │ ├── evaluators.js
│ │ ├── logger.js
│ │ ├── open2.js
│ │ ├── signals.js
│ │ └── utils.js
│ └── templates
│ │ ├── edit_application.html
│ │ ├── edit_condition.html
│ │ ├── edit_rule.html
│ │ ├── edit_settings.html
│ │ ├── list_of_applications.html
│ │ └── list_of_rules.html
└── bower.json
└── tests
├── e2e
└── main.test.js
├── test_data
└── Applications
│ ├── Calculator.app
│ └── Contents
│ │ └── Info.plist
│ ├── Google Chrome.app
│ └── Contents
│ │ ├── Info.plist
│ │ └── Resources
│ │ └── app.icns
│ └── Safari.app
│ └── Contents
│ ├── Info.plist
│ └── Resources
│ └── compass.icns
└── unit
├── evaluators.test.js
├── open2.test.js
└── utils.test.js
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "cwd": "./src",
3 | "directory": "bower_components"
4 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "plugins": [
4 | "standard",
5 | "promise"
6 | ],
7 | "env": {
8 | "browser": true,
9 | "node": true,
10 | "es6": true,
11 | "mocha": true,
12 | "amd": true
13 | },
14 | "globals": {
15 | "angular": true
16 | },
17 | "rules": {
18 | "quotes": [2, "single"],
19 | "semi": 0
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | First off, thanks for taking the time to contribute! :+1:
4 | When contributing to this repository, please first discuss the change you wish to make via issue,
5 | email, or any other method with the owners of this repository before making a change.
6 |
7 | Please note we have a code of conduct, please follow it in all your interactions with the project.
8 |
9 | ## Pull Request Process
10 |
11 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a
12 | build.
13 | 2. Update the README.md with details of changes to the interface, this includes new environment
14 | variables, exposed ports, useful file locations and container parameters.
15 | 3. Increase the version numbers in any examples files and the README.md to the new version that this
16 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
17 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
18 | do not have permission to do that, you may request the second reviewer to merge it for you.
19 |
20 | ## Code of Conduct
21 |
22 | ### Our Pledge
23 |
24 | In the interest of fostering an open and welcoming environment, we as
25 | contributors and maintainers pledge to making participation in our project and
26 | our community a harassment-free experience for everyone, regardless of age, body
27 | size, disability, ethnicity, gender identity and expression, level of experience,
28 | nationality, personal appearance, race, religion, or sexual identity and
29 | orientation.
30 |
31 | ### Our Standards
32 |
33 | Examples of behavior that contributes to creating a positive environment
34 | include:
35 |
36 | * Using welcoming and inclusive language
37 | * Being respectful of differing viewpoints and experiences
38 | * Gracefully accepting constructive criticism
39 | * Focusing on what is best for the community
40 | * Showing empathy towards other community members
41 |
42 | Examples of unacceptable behavior by participants include:
43 |
44 | * The use of sexualized language or imagery and unwelcome sexual attention or
45 | advances
46 | * Trolling, insulting/derogatory comments, and personal or political attacks
47 | * Public or private harassment
48 | * Publishing others' private information, such as a physical or electronic
49 | address, without explicit permission
50 | * Other conduct which could reasonably be considered inappropriate in a
51 | professional setting
52 |
53 | ### Our Responsibilities
54 |
55 | Project maintainers are responsible for clarifying the standards of acceptable
56 | behavior and are expected to take appropriate and fair corrective action in
57 | response to any instances of unacceptable behavior.
58 |
59 | Project maintainers have the right and responsibility to remove, edit, or
60 | reject comments, commits, code, wiki edits, issues, and other contributions
61 | that are not aligned to this Code of Conduct, or to ban temporarily or
62 | permanently any contributor for other behaviors that they deem inappropriate,
63 | threatening, offensive, or harmful.
64 |
65 | ### Scope
66 |
67 | This Code of Conduct applies both within project spaces and in public spaces
68 | when an individual is representing the project or its community. Examples of
69 | representing a project or community include using an official project e-mail
70 | address, posting via an official social media account, or acting as an appointed
71 | representative at an online or offline event. Representation of a project may be
72 | further defined and clarified by project maintainers.
73 |
74 | ### Enforcement
75 |
76 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
77 | reported by contacting the project owner/team. All
78 | complaints will be reviewed and investigated and will result in a response that
79 | is deemed necessary and appropriate to the circumstances. The project team is
80 | obligated to maintain confidentiality with regard to the reporter of an incident.
81 | Further details of specific enforcement policies may be posted separately.
82 |
83 | Project maintainers who do not follow or enforce the Code of Conduct in good
84 | faith may face temporary or permanent repercussions as determined by other
85 | members of the project's leadership.
86 |
87 | ### Attribution
88 |
89 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
90 | available at [http://contributor-covenant.org/version/1/4][version]
91 |
92 | [homepage]: http://contributor-covenant.org
93 | [version]: http://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 | **Expected behavior**
3 |
4 | **Actual behavior**
5 |
6 | **Steps to reproduce**
7 |
8 |
9 | **Web developer console logs**
10 |
11 |
12 | **Application logs**
13 |
14 |
15 | **Environment**
16 |
17 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 | **What?**
3 |
4 |
5 | **Why?**
6 |
7 |
8 | **How?**
9 |
10 |
11 | **Environment**
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | assets/
2 | build_app.sh
3 | certificates/
4 | dev_files/
5 | dist/
6 | node_modules/
7 | scripts/
8 | src/bower_components/
9 |
10 | # Created by .ignore support plugin (hsz.mobi)
11 | ### Node template
12 | # Logs
13 | logs
14 | npm-debug.log*
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (http://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | jspm_packages
39 |
40 | # Optional npm cache directory
41 | .npm
42 |
43 | # Optional eslint cache
44 | .eslintcache
45 |
46 | # Optional REPL history
47 | .node_repl_history
48 |
49 | # Yarn Integrity file
50 | .yarn-integrity
51 |
52 | ### JetBrains template
53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
54 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
55 |
56 | # User-specific stuff:
57 | .idea/tasks.xml
58 |
59 | # Sensitive or high-churn files:
60 | .idea/dataSources/
61 | .idea/dataSources.ids
62 | .idea/dataSources.xml
63 | .idea/dataSources.local.xml
64 | .idea/sqlDataSources.xml
65 | .idea/dynamic.xml
66 | .idea/uiDesigner.xml
67 |
68 | # Gradle:
69 | .idea/gradle.xml
70 | .idea/libraries
71 |
72 | # Mongo Explorer plugin:
73 | .idea/mongoSettings.xml
74 |
75 | ## File-based project format:
76 | *.iws
77 |
78 | ## Plugin-specific files:
79 |
80 | # IntelliJ
81 | /out/
82 |
83 | # mpeltonen/sbt-idea plugin
84 | .idea_modules/
85 |
86 | # JIRA plugin
87 | atlassian-ide-plugin.xml
88 |
89 | # Crashlytics plugin (for Android Studio and IntelliJ)
90 | com_crashlytics_export_strings.xml
91 | crashlytics.properties
92 | crashlytics-build.properties
93 | fabric.properties
94 | ### Example user template
95 |
96 | # IntelliJ project files
97 | .idea
98 |
99 | gen### Example user template template
100 | ### Example user template
101 |
102 | gen### macOS template
103 | *.DS_Store
104 | .AppleDouble
105 | .LSOverride
106 |
107 | # Icon must end with two \r
108 | Icon
109 |
110 |
111 | # Thumbnails
112 | ._*
113 |
114 | # Files that might appear in the root of a volume
115 | .DocumentRevisions-V100
116 | .fseventsd
117 | .Spotlight-V100
118 | .TemporaryItems
119 | .Trashes
120 | .VolumeIcon.icns
121 | .com.apple.timemachine.donotpresent
122 |
123 | # Directories potentially created on remote AFP share
124 | .AppleDB
125 | .AppleDesktop
126 | Network Trash Folder
127 | Temporary Items
128 | .apdisk
129 |
130 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '6'
4 | os:
5 | - osx
6 | env:
7 | matrix:
8 | - TARGET_ARCH=x64
9 | osx_image: xcode7.3
10 | addons:
11 | apt:
12 | packages:
13 | - xvfb
14 | before_install:
15 | - export DISPLAY=:99.0
16 | - Xvfb :99 -screen 0 1440x900x24 > /dev/null 2>&1 &
17 | cache:
18 | directories:
19 | - node_modules
20 | install:
21 | - nvm install 6
22 | - npm install
23 | - npm prune
24 | - npm install -g bower
25 | - bower install --allow-root
26 | - bower prune
27 | script:
28 | - npm test
29 | before_deploy:
30 | - npm run dist-unsigned
31 | deploy:
32 | provider: releases
33 | skip_cleanup: true
34 | all_branches: true
35 | overwrite: true
36 | file_glob: true
37 | file: dist/mac/BrowserDispatcher-*
38 | on:
39 | tags: true
40 | api-key:
41 | secure: TkvCIOlbpJdRYSquCVCtMEzQvJOgBVawMQpdm3KqQuhUU3tkvZnhpeIkjiDOq/Z8y4wOI6N+TpXB59HTrqTs7p2AiciR2MvpLq4cwsice2HhCTRkjJ6LgioI8gM52bj3H8V97KgEcX34FhTXDjFAav8W1mzm6oEtoFEEls96ynNnd66Tqm9czGEQkhQcscJfMtyNr/JPHN6YiozBVly/BjSEAqTMtIpd4V9htfx/zt10ZeHMXx0e/Xv+ZJ1G5KHq/IhLwRicWQ/XUFTzRXxn66pQZO0caYJtoFR8iI7fTa5Cir8z98/bvIaUS4XvYN3Zt1Kw/tIgmdjHN8+oJrpkx0+M3IYdcb/GaC3ketftah8smZmT+hYNixwTjyf8ouv5reAlp4J446F0AJA46RMllbiAWVx0NPuzysCwUExSCKe2FgN32cY8vOHolnodPY8opCiqEVhCKEelOVqCwPU2pVTalcuu1hsT+l2X0/bK0HhqNtkjnNXTh42mDeKw4TSqFA4jHmxUlPnB8rvq08zpee9JET6cdH6Db02LS66bOFQfBXKaoEyQZR8nNeUyk6afSFYaoWwo2Rm7HWwmJIHnAjIRu8p/iL96VZ6NlWCGZLrHDMpU8YkPikWmfw1Ft1DeSWpuORsbYmkBpeCKs889TZQSVbA/XIyNqp/oILAXsgs=
42 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/CONTRIBUTORS.md
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Andriy Hrytskiv
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/andriyko/browser-dispatcher)
2 |
3 | # Browser Dispatcher
4 | *BrowserDispatcher.app* decides which browser to use when you click on a link outside of your web browser.
5 | Based on your settings, it will display a prompt allowing you to select from a list of browsers or automatically select a browser to open a link with.
6 |
7 | 
8 |
9 | ## Downloads
10 | To download the latest release, see [releases page](https://github.com/andriyko/browser-dispatcher/releases) (macOS only).
11 |
12 | **Note:** the application is not signed by Apple Developer ID.
13 | By default, Mac OS will not open an app from an unidentified developer.
14 |
15 | In the Finder, locate *BrowserDispatcher.app* and Control-click the app icon, then choose Open from the shortcut menu.
16 | Make *BrowserDispatcher.app* default web browser. You can use the app menu `Preferences > Make default browser` or go to `System Preferences > General > Default web browser`.
17 |
18 | The application is an agent app, that is, menu bar app that does not appear in the Dock or Force Quit window.
19 |
20 | ## Running from source
21 | You'll need certain packages installed before you can build and run Browser Dispatcher locally.
22 |
23 | ### Prerequisites
24 | 1. `nodejs >= 6.2`
25 |
26 | Install from your package manager or download from https://nodejs.org
27 |
28 | 2. `npm install -g bower`
29 |
30 | ### Installation
31 |
32 | After installing the prerequisites:
33 |
34 | 1. Clone the git repository from GitHub:
35 | ```
36 | git clone git@github.com:brave/browser-dispatcher.git
37 | ```
38 |
39 | 2. Open the working directory:
40 | ```
41 | cd browser-dispatcher
42 | ```
43 | 3. Install dependencies:
44 |
45 | ```
46 | npm install
47 | bower install
48 | ```
49 |
50 | To start the application in development mode run `npm start` or `electron .`.
51 |
52 | ## Running the tests
53 |
54 | Run unit and end-to-end tests:
55 | ```
56 | nmp test
57 | ```
58 |
59 | Run unit tests only:
60 | ```
61 | npm run test:unit
62 | ```
63 |
64 | Run end-to-end tests only:
65 | ```
66 | npm run test:e2e
67 | ```
68 |
69 |
70 | ## Building the application
71 |
72 | Build without code-signing:
73 | ```
74 | npm run dist-unsigned
75 | ```
76 |
77 | Build and code-sign:
78 | ```
79 | npm run dist
80 | ```
81 |
82 | ## Built With
83 |
84 | * [Electron](http://electron.atom.io) - The framework for creating native applications with web technologies like JavaScript, HTML, and CSS.
85 | * [Photon](http://photonkit.com) - The fastest way to build beautiful Electron apps using simple HTML and CSS.
86 |
87 | ## Contributing
88 |
89 | Please read [CONTRIBUTING.md](https://github.com/andriyko/browser-dispatcher/blob/master/.github/CONTRIBUTING.md) for details on code of conduct, and the process for submitting pull requests.
90 |
91 | ## Versioning
92 |
93 | For the versions available, see the [tags on this repository](https://github.com/andriyko/browser-dispatcher/tags).
94 |
95 | ## Authors
96 |
97 | * [andriyko](https://github.com/andriyko)
98 |
99 | See also the list of [contributors](CONTRIBUTORS.md) who participated in this project.
100 |
101 | ## License
102 |
103 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
104 |
--------------------------------------------------------------------------------
/build/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/build/background.png
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/build/icon.icns
--------------------------------------------------------------------------------
/build_env.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | npm install
4 | bower install
5 |
6 | npm test
7 |
--------------------------------------------------------------------------------
/build_files/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSUIElement
6 | 1
7 |
8 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Browser Dispatcher
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
56 |
57 |
81 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/index2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | BrowserDispatcher
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
23 | close
24 |
25 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const iconutil = require('iconutil');
6 | const electron = require('electron');
7 | const {app, dialog, ipcMain, Menu, Tray, BrowserWindow} = require('electron');
8 | const isDev = require('electron-is-dev');
9 | const Promise = require('bluebird');
10 |
11 | const {getLogger} = require('./src/app/js/logger');
12 | const evaluators = require('./src/app/js/evaluators');
13 | const signals = require('./src/app/js/signals');
14 | const docs = require('./src/app/js/docs');
15 | const utils = require('./src/app/js/utils');
16 | const CONST = require('./src/app/js/constants');
17 | const open2 = require('./src/app/js/open2');
18 |
19 | app.setName(CONST.COMMON.APP_NAME);
20 |
21 | // global object for app tray
22 | let appTray = null;
23 |
24 | const logger = getLogger(path.join(app.getPath('userData'), `${CONST.COMMON.APP_NAME}.log`));
25 | const sqlodm = new docs.CamoWrapper(app.getPath('userData'));
26 |
27 | // Keep a global reference of the window object, if you don't, the window will
28 | // be closed automatically when the JavaScript object is garbage collected.
29 | let mainWindow, secondaryWindow;
30 |
31 | let handleRedirect = (e, url) => {
32 | if (mainWindow && url !== mainWindow.webContents.getURL()) {
33 | e.preventDefault();
34 | openUrlInDefaultBrowser(url);
35 | }
36 | };
37 |
38 | function createMainWindow () {
39 | // Only single main window is allowed
40 | if (mainWindow) {
41 | mainWindow.focus();
42 | return;
43 | }
44 | // Create the browser window.
45 | mainWindow = new BrowserWindow(
46 | {
47 | width: 930,
48 | height: 530,
49 | resizable: true,
50 | show: false
51 | }
52 | );
53 |
54 | // and load the index.html of the app.
55 | mainWindow.loadURL(`file://${__dirname}/index.html`);
56 |
57 | mainWindow.webContents.on('crashed', (event, killed) => {
58 | logger.error(`The application has crashed. Killed: ${killed}. Event: ${event}`);
59 | let opts = {
60 | type: 'info',
61 | title: 'The application has crashed',
62 | message: 'This process has crashed',
63 | buttons: ['Reload', 'Close']
64 | };
65 | dialog.showMessageBox(opts, (index) => {
66 | if (index === 0) {
67 | mainWindow.reload();
68 | } else {
69 | mainWindow.close();
70 | }
71 | });
72 | });
73 |
74 | mainWindow.webContents.on('will-navigate', handleRedirect);
75 | mainWindow.webContents.on('new-window', handleRedirect);
76 |
77 | mainWindow.on('unresponsive', () => {
78 | logger.error('The application is not responding');
79 | let opts = {
80 | type: 'info',
81 | title: 'The application is not responding',
82 | message: 'Reload the window?',
83 | buttons: ['Reload', 'Cancel']
84 | };
85 | require('dialog').showMessageBox(opts, (index) => {
86 | if (index === 0) {
87 | mainWindow.reload();
88 | } else {
89 | mainWindow.close();
90 | }
91 | });
92 | });
93 |
94 | mainWindow.on('ready-to-show', () => {
95 | mainWindow.show();
96 | mainWindow.focus();
97 | });
98 |
99 | // Emitted when the window is closed.
100 | mainWindow.on('closed', () => {
101 | // Dereference the window object, usually you would store windows
102 | // in an array if your app supports multi windows, this is the time
103 | // when you should delete the corresponding element.
104 | mainWindow = null;
105 | });
106 | }
107 |
108 | function createSecondaryWindow (url) {
109 | if (secondaryWindow) {
110 | secondaryWindow.show();
111 | secondaryWindow.focus();
112 | } else {
113 | let {x, y} = electron.screen.getCursorScreenPoint();
114 | secondaryWindow = new BrowserWindow(
115 | {
116 | x: x,
117 | y: y,
118 | closable: true,
119 | alwaysOnTop: true,
120 | movable: false,
121 | resizable: false,
122 | minimizable: false,
123 | maximizable: false,
124 | fullscreenable: false,
125 | show: false,
126 | frame: false,
127 | height: 100,
128 | width: 499,
129 | useContentSize: true,
130 | transparent: true
131 | }
132 | );
133 | secondaryWindow.loadURL(`file://${__dirname}/index2.html`);
134 |
135 | // TODO add appropriate handler
136 | secondaryWindow.webContents.on('crashed', () => {
137 | logger.error('The app has crashed', {'window': 'createSecondaryWindow'});
138 | });
139 |
140 | // TODO add appropriate handler
141 | secondaryWindow.on('unresponsive', () => {
142 | logger.error('The app is unresponsive', {'window': 'createSecondaryWindow'});
143 | });
144 |
145 | // Emitted when the window is closed.
146 | secondaryWindow.on('closed', () => {
147 | // Dereference the window object, usually you would store windows
148 | // in an array if your app supports multi windows, this is the time
149 | // when you should delete the corresponding element.
150 | secondaryWindow = null;
151 | });
152 | }
153 |
154 | secondaryWindow.webContents.on('did-finish-load', () => {
155 | secondaryWindow.webContents.send(signals.response(signals.GENERAL.LOAD_URL), url);
156 | });
157 | }
158 |
159 | const shouldQuit = app.makeSingleInstance((commandLine, workingDirectory) => {
160 | // Someone tried to run a second instance, we should focus our window.
161 | if (mainWindow) {
162 | if (mainWindow.isMinimized()) {
163 | mainWindow.restore();
164 | }
165 | mainWindow.focus();
166 | }
167 | });
168 |
169 | if (shouldQuit) {
170 | app.quit();
171 | }
172 |
173 | function buildAppMenu () {
174 | let template = [
175 | {
176 | label: `${CONST.COMMON.APP_NAME}`,
177 | enabled: mainWindow === null || mainWindow === undefined,
178 | click: () => {
179 | createMainWindow();
180 | }
181 | },
182 | {
183 | label: 'Preferences',
184 | submenu: [
185 | {
186 | label: 'Open at login',
187 | type: 'checkbox',
188 | checked: app.getLoginItemSettings()['openAtLogin'],
189 | click: () => {
190 | if (app.getLoginItemSettings()['openAtLogin']) {
191 | app.setLoginItemSettings({openAtLogin: false, openAsHidden: false});
192 | } else {
193 | app.setLoginItemSettings({openAtLogin: true, openAsHidden: true});
194 | }
195 | }
196 | },
197 | {
198 | label: 'Make default browser',
199 | type: 'checkbox',
200 | checked: app.isDefaultProtocolClient('http'),
201 | click: () => {
202 | if (app.isDefaultProtocolClient('http')) {
203 | app.removeAsDefaultProtocolClient('http');
204 | app.removeAsDefaultProtocolClient('https');
205 | } else {
206 | app.setAsDefaultProtocolClient('http');
207 | app.setAsDefaultProtocolClient('https');
208 | }
209 | }
210 | }
211 | ]
212 | },
213 | {
214 | label: 'Edit',
215 | // visible: mainWindow !== null && mainWindow !== undefined,
216 | submenu: [
217 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
218 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
219 | { type: 'separator' },
220 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
221 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
222 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
223 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' }
224 | ]
225 | },
226 | {
227 | label: 'View',
228 | submenu: [
229 | {
230 | label: 'Reload',
231 | // visible: mainWindow !== null && mainWindow !== undefined,
232 | accelerator: 'CmdOrCtrl+R',
233 | click: (item, focusedWindow) => {
234 | if (focusedWindow) {
235 | // on reload, start fresh and close any old
236 | // open secondary windows
237 | if (focusedWindow.id === 1) {
238 | BrowserWindow.getAllWindows().forEach((win) => {
239 | if (win.id > 1) {
240 | win.close()
241 | }
242 | })
243 | }
244 | focusedWindow.reload()
245 | }
246 | }
247 | },
248 | {
249 | label: 'Show applications selector',
250 | accelerator: 'CmdOrCtrl+L',
251 | click: () => { createSecondaryWindow('http://google.com'); }
252 | },
253 | {
254 | label: 'Toggle Developer Tools',
255 | // visible: mainWindow !== null && mainWindow !== undefined,
256 | accelerator: 'Alt+Command+I',
257 | click: (item, focusedWindow) => {
258 | // if (focusedWindow && isDev) {
259 | if (focusedWindow) {
260 | focusedWindow.toggleDevTools()
261 | }
262 | }
263 | }
264 | ]
265 | },
266 | { type: 'separator' },
267 | { label: 'About', selector: 'orderFrontStandardAboutPanel:', role: 'about' },
268 | {
269 | label: 'Quit',
270 | accelerator: 'Command+Q',
271 | selector: 'terminate:',
272 | click: () => {
273 | if (mainWindow !== undefined && mainWindow !== null) {
274 | mainWindow.close();
275 | }
276 | app.quit();
277 | }
278 | }
279 | ];
280 | return Menu.buildFromTemplate(template);
281 | }
282 |
283 | function buildTray () {
284 | if (!appTray) {
285 | appTray = new Tray(`${__dirname}/src/app/images/tray_icon.png`);
286 | }
287 | appTray.setToolTip(CONST.COMMON.APP_NAME);
288 | // appTray.setContextMenu(buildContextMenu());
289 | let menu = buildAppMenu();
290 | Menu.setApplicationMenu(menu);
291 | appTray.setContextMenu(menu);
292 | }
293 |
294 | function autoLoadBrowsers () {
295 | sqlodm.application.count().then(
296 | count => {
297 | if (count === 0) {
298 | let appsRoot = process.env.BROWSER_DISPATCHER_APPS_ROOT || '/Applications';
299 | let {results, errors} = utils.getApps(appsRoot);
300 | if (errors.length) {
301 | logger.warn(`There were ${errors.length} errors while fetching the list of applications`);
302 | }
303 | for (let b of results) {
304 | if (b.name !== CONST.COMMON.APP_NAME) {
305 | let application = sqlodm.application.create({
306 | name: b.name,
307 | display_name: b.display_name,
308 | path: b.path,
309 | icns: b.icns,
310 | executable: b.executable,
311 | identifier: b.identifier,
312 | is_default: (b.name === 'Safari')
313 | });
314 | application.save().then(
315 | result => {
316 | result = result.toJSON();
317 | logger.info(`Added application ${result.name}`);
318 | iconutil.toIconset(b.icns, (err, icons) => {
319 | if (!err) {
320 | for (let [k, v] of utils.entries(icons)) {
321 | sqlodm.icon.create({name: k, application: result._id, content: v}).save().then(
322 | ico => {
323 | logger.info(`Added icon ${ico.name} for application ${result.name}`);
324 | },
325 | error => {
326 | logger.error(`Failed to add icon for the application "${b.name}". Error: `, error);
327 | }
328 | );
329 | }
330 | }
331 | });
332 | },
333 | error => {
334 | logger.error(`Failed to add application "${b.name}". Error: `, error);
335 | }
336 | );
337 | }
338 | }
339 | }
340 | },
341 | error => {
342 | logger.error('Failed to count configured applications. Error: ', error);
343 | }
344 | );
345 | }
346 |
347 | function appReady () {
348 | sqlodm.init().then(
349 | () => {
350 | logger.info('Application started.');
351 | autoLoadBrowsers();
352 | require('electron-debug')({showDevTools: isDev});
353 | },
354 | error => {
355 | logger.error('Failed to initialize database during application start. Error:', error);
356 | }
357 | );
358 |
359 | buildTray();
360 | createMainWindow();
361 | }
362 |
363 | // This method will be called when Electron has finished
364 | // initialization and is ready to create browser windows.
365 | app.on('ready', appReady);
366 |
367 | // Quit when all windows are closed.
368 | app.on('window-all-closed', () => {
369 | // On OS X it is common for applications and their menu bar
370 | // to stay active until the user quits explicitly with Cmd + Q
371 | if (process.platform !== 'darwin') {
372 | app.quit();
373 | }
374 | });
375 |
376 | function openUrl (rule, url) {
377 | let aORb = '-b';
378 | let options = [];
379 | let application = rule.application.identifier;
380 |
381 | if (rule.use_app_executable) {
382 | aORb = '-a';
383 | application = rule.application.executable;
384 | }
385 | if (rule.open_new_instance) {
386 | options.push('-n');
387 | }
388 | if (rule.open_not_foreground) {
389 | options.push('-g');
390 | }
391 | if (rule.open_fresh) {
392 | options.push('-F');
393 | }
394 | open2(url, application, aORb, options.join(' '), rule.open_args);
395 | }
396 |
397 | function openUrlInDefaultBrowser (url) {
398 | sqlodm.application.findOne({is_default: true}).then(
399 | application => {
400 | open2(url, application.identifier, '-b');
401 | },
402 | error => {
403 | logger.error('[open-url] Failed to get default browser to use. Error: ', error);
404 | createSecondaryWindow(url);
405 | }
406 | );
407 | }
408 |
409 | app.on('open-file', (event, f) => {
410 | event.preventDefault();
411 | sqlodm.application.findOne({is_default: true}).then(
412 | application => {
413 | open2(f, application.identifier, '-b');
414 | },
415 | error => {
416 | logger.error('Failed to open file. Error: ', error);
417 | createSecondaryWindow(f);
418 | }
419 | );
420 | });
421 |
422 | app.on('open-url', (event, url) => {
423 | event.preventDefault();
424 | sqlodm.prefs.find().then(
425 | results => {
426 | let appStatus = results.find(item => { return item.name === CONST.STATUS.IS_APP_ENABLED });
427 | let useDefault = results.find(item => { return item.name === CONST.STATUS.IS_USE_DEFAULT });
428 | if (appStatus.status) {
429 | sqlodm.rule.find({is_active: true}, {populate: true}).then(
430 | rules => {
431 | for (let rule of rules) {
432 | rule.conditions = rule.conditions.filter((cond) => {
433 | return cond.is_active;
434 | })
435 | }
436 | let rule = evaluators.evaluateRules(rules, url, event);
437 | if (rule) {
438 | openUrl(rule, url);
439 | } else {
440 | if (useDefault.status) {
441 | openUrlInDefaultBrowser(url);
442 | } else {
443 | createSecondaryWindow(url);
444 | }
445 | }
446 | },
447 | error => {
448 | createSecondaryWindow(url);
449 | logger.error('[open-url] Failed to get the list of rules. Error: ', error);
450 | }
451 | )
452 | } else {
453 | createSecondaryWindow(url);
454 | }
455 | },
456 | error => {
457 | logger.error('[open-url] Failed to get configuration. Error: ', error);
458 | createSecondaryWindow(url);
459 | }
460 | )
461 | });
462 |
463 | app.on('activate', () => {
464 | // On OS X it's common to re-create a window in the app when the
465 | // dock icon is clicked and there are no other windows open.
466 | if (mainWindow === null) {
467 | createMainWindow();
468 | }
469 | });
470 |
471 | // Register listeners
472 |
473 | // Icons
474 | ipcMain.on(signals.request(signals.ICON.READ), (event, arg) => {
475 | return sqlodm.icon.find(arg.query, arg.options).then(
476 | result => {
477 | event.sender.send(signals.response(signals.ICON.READ), result);
478 | },
479 | error => {
480 | logger.error(`[${signals.ICON.READ}] Error: `, error);
481 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
482 | }
483 | );
484 | });
485 |
486 | // Apps/Browsers
487 | ipcMain.on(signals.request(signals.APPLICATION.READ), (event, arg) => {
488 | sqlodm.application.find(arg.query, arg.options).then(
489 | result => {
490 | result.forEach((elem, idx, result) => {
491 | result[idx] = Object.assign({}, elem.toJSON());
492 | });
493 | event.sender.send(signals.response(signals.APPLICATION.READ), result);
494 | },
495 | error => {
496 | logger.error(`[${signals.APPLICATION.READ}] Error: `, error);
497 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
498 | }
499 | );
500 | });
501 |
502 | ipcMain.on(signals.request(signals.APPLICATION.UPDATE_DEFAULT), (event, arg) => {
503 | sqlodm.application.findOneAndUpdate({is_default: true}, {is_default: false}, {}).then(
504 | result => {
505 | sqlodm.application.findOneAndUpdate(arg.query, {is_default: true}, {}).then(
506 | res => {
507 | event.sender.send(signals.response(signals.APPLICATION.UPDATE_DEFAULT), res);
508 | logger.info(`Changed default browser from "${result.name}" to "${res.name}"`)
509 | },
510 | err => {
511 | logger.error(`[${signals.APPLICATION.UPDATE_DEFAULT}] Failed to change default browser. Error: `, err);
512 | event.sender.send(signals.response(signals.GENERAL.ERROR), err);
513 | }
514 | );
515 | },
516 | error => {
517 | logger.error(`[${signals.APPLICATION.UPDATE_DEFAULT}] Error: `, error);
518 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
519 | }
520 | );
521 | });
522 |
523 | ipcMain.on(signals.request(signals.APPLICATION.READ_ONE), (event, arg) => {
524 | sqlodm.application.findOne(arg.query, arg.options).then(
525 | result => {
526 | event.sender.send(signals.response(signals.APPLICATION.READ_ONE), result);
527 | },
528 | error => {
529 | logger.error(`[${signals.APPLICATION.READ_ONE}] Error: `, error);
530 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
531 | }
532 | );
533 | });
534 |
535 | ipcMain.on(signals.request(signals.APPLICATION.DELETE_ONE), (event, arg) => {
536 | sqlodm.application.deleteOne(arg.query).then(
537 | () => {
538 | Promise.all([
539 | sqlodm.icon.deleteMany({application: arg.query._id}),
540 | sqlodm.rule.deleteMany({application: arg.query._id})
541 | ]).then(
542 | result => {
543 | event.sender.send(signals.response(signals.APPLICATION.DELETE_ONE), result);
544 | logger.info(`Removed application: ${arg.query._id}`);
545 | },
546 | error => {
547 | logger.error(`[${signals.APPLICATION.DELETE_ONE}] Failed to remove application ${arg.query._id}. Error: `, error);
548 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
549 | }
550 | )
551 | },
552 | error => {
553 | logger.error(`[${signals.APPLICATION.DELETE_ONE}] Error: `, error);
554 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
555 | }
556 | );
557 | });
558 |
559 | ipcMain.on(signals.request(signals.APPLICATION.CREATE_ONE), (event, arg) => {
560 | sqlodm.application.create(arg.values).save().then(
561 | result => {
562 | result = Object.assign({}, result.toJSON());
563 |
564 | iconutil.toIconset(result.icns,
565 | (err, icons) => {
566 | if (!err) {
567 | for (let [k, v] of utils.entries(icons)) {
568 | sqlodm.icon.create({name: k, application: result._id, content: v}).save().then(
569 | icon => {
570 | logger.info(`Added icon ${icon.name} for application ${result.name}`);
571 | },
572 | error => {
573 | logger.error(`[${signals.APPLICATION.CREATE_ONE}] Failed to add icon for application. Error: `, error);
574 | }
575 | );
576 | }
577 | event.sender.send(signals.response(signals.APPLICATION.CREATE_ONE), result);
578 | logger.info(`Added application: ${result.name}`);
579 | } else {
580 | event.sender.send(signals.response(signals.APPLICATION.CREATE_ONE), result);
581 | }
582 | }
583 | );
584 | },
585 | error => {
586 | logger.error(`[${signals.APPLICATION.CREATE_ONE}]. Error: `, error);
587 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
588 | }
589 | )
590 | });
591 |
592 | ipcMain.on(signals.request(signals.APPLICATION.UPDATE_ONE), (event, arg) => {
593 | let values = sqlodm.application._fromData(arg.values).toJSON();
594 | sqlodm.application.findOneAndUpdate(arg.query, values).then(
595 | result => {
596 | result = Object.assign({}, result.toJSON());
597 | event.sender.send(signals.response(signals.APPLICATION.UPDATE_ONE), result);
598 | logger.info(`Updated application: ${result.name}`);
599 | },
600 | error => {
601 | logger.error(`[${signals.APPLICATION.UPDATE_ONE}]. Error: `, error);
602 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
603 | }
604 | );
605 | });
606 |
607 | ipcMain.on(signals.request(signals.APPLICATION.UPDATE), (event, arg) => {
608 | sqlodm.application.find(arg.query).then(
609 | result => {
610 | result.forEach(item => {
611 | Object.assign(item, arg.values);
612 | item.save();
613 | });
614 | event.sender.send(signals.response(signals.APPLICATION.UPDATE), result);
615 | },
616 | error => {
617 | logger.error(`[${signals.APPLICATION.UPDATE}]. Error: `, error);
618 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
619 | }
620 | );
621 | });
622 |
623 | // Settings/Prefs
624 | ipcMain.on(signals.request(signals.CONFIG.READ_STATUS), (event, arg) => {
625 | sqlodm.prefs.findOne(arg.query).then(
626 | result => {
627 | result = Object.assign({}, result.toJSON());
628 | event.sender.send(signals.response(signals.CONFIG.READ_STATUS), result);
629 | },
630 | error => {
631 | logger.error(`[${signals.CONFIG.READ_STATUS}]. Error: `, error);
632 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
633 | }
634 | );
635 | });
636 |
637 | ipcMain.on(signals.request(signals.CONFIG.UPDATE_STATUS), (event, arg) => {
638 | sqlodm.prefs.findOneAndUpdate(arg.query, arg.values).then(
639 | result => {
640 | result = Object.assign({}, result.toJSON());
641 | event.sender.send(signals.response(signals.CONFIG.UPDATE_STATUS), result);
642 | logger.info(result.status ? 'Enabled app' : 'Disabled app');
643 | },
644 | error => {
645 | logger.error(`[${signals.CONFIG.UPDATE_STATUS}]. Error: `, error);
646 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
647 | }
648 | );
649 | });
650 |
651 | ipcMain.on(signals.request(signals.CONFIG.READ_IS_USE_DEFAULT), (event, arg) => {
652 | sqlodm.prefs.findOne(arg.query).then(
653 | result => {
654 | result = Object.assign({}, result.toJSON());
655 | event.sender.send(signals.response(signals.CONFIG.READ_IS_USE_DEFAULT), result);
656 | },
657 | error => {
658 | logger.error(`[${signals.CONFIG.READ_IS_USE_DEFAULT}]. Error: `, error);
659 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
660 | }
661 | );
662 | });
663 |
664 | ipcMain.on(signals.request(signals.CONFIG.UPDATE_IS_USE_DEFAULT), (event, arg) => {
665 | sqlodm.prefs.findOneAndUpdate(arg.query, arg.values).then(
666 | result => {
667 | result = Object.assign({}, result.toJSON());
668 | event.sender.send(signals.response(signals.CONFIG.UPDATE_IS_USE_DEFAULT), result);
669 | let action = result.status ? 'Enabled' : 'Disabled';
670 | logger.info(`${action} usage of default browser if none of the rules match an URL`);
671 | },
672 | error => {
673 | logger.error(`[${signals.CONFIG.UPDATE_IS_USE_DEFAULT}]. Error: `, error);
674 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
675 | }
676 | );
677 | });
678 |
679 | // Rules
680 | ipcMain.on(signals.request(signals.RULE.READ), (event, arg) => {
681 | sqlodm.rule.find(arg.query, arg.options).then(
682 | result => {
683 | result.forEach((elem, idx, result) => {
684 | result[idx] = elem.toJSON();
685 | });
686 | event.sender.send(signals.response(signals.RULE.READ), result);
687 | },
688 | error => {
689 | logger.error(`[${signals.RULE.READ}]. Error: `, error);
690 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
691 | }
692 | );
693 | });
694 |
695 | ipcMain.on(signals.request(signals.RULE.CREATE_ONE), (event, arg) => {
696 | sqlodm.rule.create(arg.values).preValidate().save().then(
697 | result => {
698 | result = Object.assign({}, result.toJSON());
699 | event.sender.send(signals.response(signals.RULE.CREATE_ONE), result);
700 | logger.info(`Added rule: ${result._id}`);
701 | },
702 | error => {
703 | logger.error(`[${signals.RULE.CREATE_ONE}]. Error: `, error);
704 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
705 | }
706 | );
707 | });
708 |
709 | ipcMain.on(signals.request(signals.RULE.UPDATE_ONE), (event, arg) => {
710 | let values = sqlodm.rule.create(arg.values).preValidate().toJSON();
711 | sqlodm.rule.findOneAndUpdate(arg.query, values, arg.options).then(
712 | result => {
713 | result = Object.assign({}, result.toJSON());
714 | event.sender.send(signals.response(signals.RULE.UPDATE_ONE), result);
715 | logger.info(`Updated rule: ${result._id}`);
716 | },
717 | error => {
718 | logger.error(`[${signals.RULE.UPDATE_ONE}]. Error: `, error);
719 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
720 | }
721 | );
722 | });
723 |
724 | ipcMain.on(signals.request(signals.RULE.DELETE_ONE), (event, arg) => {
725 | sqlodm.rule.deleteOne(arg.query).then(
726 | result => {
727 | event.sender.send(signals.response(signals.RULE.DELETE_ONE), result);
728 | logger.info(`Deleted rule: ${result._id}`);
729 | },
730 | error => {
731 | logger.error(`[${signals.RULE.DELETE_ONE}]. Error: `, error);
732 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
733 | }
734 | );
735 | });
736 |
737 | ipcMain.on(signals.request(signals.RULE.CLEAR), (event, arg) => {
738 | sqlodm.rule.clearCollection().then(
739 | result => {
740 | event.sender.send(signals.response(signals.RULE.CLEAR), result);
741 | },
742 | error => {
743 | logger.error(`[${signals.RULE.CLEAR}]. Error: `, error);
744 | event.sender.send(signals.response(signals.GENERAL.ERROR), error.toString());
745 | }
746 | );
747 | });
748 |
749 | ipcMain.on(signals.request(signals.CONFIG.RESET_ALL), (event, arg) => {
750 | let userDataPathDatabases = path.join(app.getPath('userData'), 'databases');
751 | for (let fpath of [
752 | path.join(userDataPathDatabases, 'applications.db'),
753 | path.join(userDataPathDatabases, 'icons.db'),
754 | path.join(userDataPathDatabases, 'rules.db'),
755 | path.join(userDataPathDatabases, 'settings.db')
756 | ]) {
757 | if (fs.existsSync(fpath) && fs.statSync(fpath).isFile()) {
758 | fs.unlinkSync(fpath);
759 | }
760 | }
761 | app.relaunch();
762 | app.exit(0);
763 | event.returnValue = 'done';
764 | });
765 |
766 | ipcMain.on(signals.request(signals.GENERAL.TEST_URL), (event, arg) => {
767 | let result = evaluators.evaluateRules(arg.rules, arg.url, event);
768 | if (result) {
769 | result = Object.assign({}, result);
770 | }
771 | event.sender.send(signals.response(signals.GENERAL.TEST_URL), result);
772 | });
773 |
774 | ipcMain.on(signals.request(signals.GENERAL.SHOW_MAN_PAGE), (event, arg) => {
775 | require('child_process').exec('man -t open | open -f -a Preview');
776 | event.sender.send(signals.response(signals.GENERAL.SHOW_MAN_PAGE), {});
777 | });
778 |
779 | // App2 signal handlers
780 | ipcMain.on(signals.request(signals.GENERAL.OPEN_URL), (event, arg) => {
781 | open2(arg.url, arg.application.identifier, '-b');
782 | secondaryWindow.close();
783 | // secondaryWindow.destroy();
784 | });
785 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BrowserDispatcher",
3 | "productName": "BrowserDispatcher",
4 | "version": "0.1.5",
5 | "description": "Sends link to the right browser depending on a set of predefined rules",
6 | "main": "main.js",
7 | "author": "Andriy Hrytskiv",
8 | "license": "MIT",
9 | "private": true,
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/andriyko/browser-dispatcher.git"
13 | },
14 | "keywords": [
15 | "Electron",
16 | "Browser"
17 | ],
18 | "bugs": {
19 | "url": "https://github.com/andriyko/browser-dispatcher/issues"
20 | },
21 | "homepage": "https://github.com/andriyko/browser-dispatcher",
22 | "dependencies": {
23 | "bluebird": "^3.4.7",
24 | "camo": "^0.12.3",
25 | "electron-debug": "^1.1.0",
26 | "electron-is-dev": "^0.1.2",
27 | "glob": "^7.1.1",
28 | "iconutil": "^1.0.1",
29 | "nedb": "^1.8.0",
30 | "plist": "^2.0.1",
31 | "winston": "^2.3.0"
32 | },
33 | "devDependencies": {
34 | "babel-cli": "^6.18.0",
35 | "babel-preset-electron": "^0.37.8",
36 | "babel-preset-es2015": "^6.18.0",
37 | "chai": "^3.5.0",
38 | "chai-as-promised": "^6.0.0",
39 | "devtron": "^1.4.0",
40 | "electron": "^1.4.15",
41 | "electron-builder": "^11.7.0",
42 | "electron-rebuild": "^1.5.6",
43 | "eslint": "^3.15.0",
44 | "eslint-config-standard": "^6.2.1",
45 | "eslint-plugin-promise": "^3.4.1",
46 | "eslint-plugin-standard": "^2.0.1",
47 | "mocha": "^3.2.0",
48 | "mock-require": "^2.0.1",
49 | "proxyquire": "^1.7.11",
50 | "spectron": "^3.6.0"
51 | },
52 | "scripts": {
53 | "start": "electron . --enable-logging",
54 | "lint": "./node_modules/.bin/eslint src/app/ && ./node_modules/.bin/eslint tests && ./node_modules/.bin/eslint main.js",
55 | "test:unit": "./node_modules/.bin/mocha tests/unit --recursive",
56 | "test:e2e": "./node_modules/.bin/mocha tests/e2e --recursive",
57 | "test": "npm run lint && npm run test:unit && npm run test:e2e",
58 | "clean": "rm -rf ./dist",
59 | "dist-unsigned": "export CSC_IDENTITY_AUTO_DISCOVERY=false && npm run clean && npm prune && bower prune && node_modules/.bin/build --mac",
60 | "dist": "npm run clean && npm prune && bower prune && node_modules/.bin/build --mac"
61 | },
62 | "build": {
63 | "appId": "com.electron.browserdispatcher",
64 | "mac": {
65 | "category": "public.app-category.productivity"
66 | },
67 | "extend-info": "build_files/Info.plist",
68 | "files": [
69 | "**/*",
70 | "!.*",
71 | "!build_env.sh",
72 | "!build_app.sh",
73 | "!package.json",
74 | "!README.md",
75 | "!CONTRIBUTORS.md",
76 | "!src/bower.json",
77 | "!src/bower_components${/*}",
78 | "!assets${/*}",
79 | "!build_files${/*}",
80 | "!certificates${/*}",
81 | "!dev_files${/*}",
82 | "!built${/*}",
83 | "!dist{/*}",
84 | "!scripts${/*}",
85 | "!tests${/*}",
86 | "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts}",
87 | "!**/node_modules/.bin",
88 | "!**/*.{o,hprof,orig,pyc,pyo,rbc}",
89 | "!**/._*",
90 | "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.gitignore,.gitattributes,.editorconfig,.flowconfig,.yarn-metadata.json,.idea,appveyor.yml,.travis.yml,circle.yml,npm-debug.log,.nyc_output,yarn.lock,.yarn-integrity}",
91 | "src/bower_components/angular/angular.min.js",
92 | "src/bower_components/angular-sanitize/angular-sanitize.min.js",
93 | "src/bower_components/angular-animate/angular-animate.min.js",
94 | "src/bower_components/angular-aria/angular-aria.min.js",
95 | "src/bower_components/angular-messages/angular-messages.min.js",
96 | "src/bower_components/angular-material/angular-material.min.js",
97 | "src/bower_components/angular-material/angular-material.min.css",
98 | "src/bower_components/flexboxgrid/dist/flexboxgrid.min.css",
99 | "src/bower_components/photon/dist/css/photon.min.css",
100 | "src/bower_components/photon/dist/fonts/*"
101 | ],
102 | "protocols": [
103 | {
104 | "name": "Web site URL",
105 | "role": "Viewer",
106 | "schemes": [
107 | "http",
108 | "https"
109 | ]
110 | }
111 | ]
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/app/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {remote, ipcRenderer} = require('electron');
4 | const {dialog} = require('electron').remote;
5 |
6 | const signals = require('./src/app/js/signals');
7 | const utils = require('./src/app/js/utils');
8 | const evaluators = require('./src/app/js/evaluators');
9 | const CONST = require('./src/app/js/constants');
10 |
11 | const myApp = angular.module('myApp', ['ngSanitize']);
12 |
13 | myApp.run(function ($rootScope) {
14 | $rootScope.ctx = {
15 | _settings: 'settings',
16 | _rules: 'rules',
17 | _apps: 'apps',
18 | _ctx: 'rules',
19 | _switchCtx: function (ctx) {
20 | this._ctx = ctx;
21 | },
22 | _isCtx: function (ctx) {
23 | return this._ctx === ctx;
24 | },
25 | isSettingsView: function () {
26 | return this._isCtx(this._settings);
27 | },
28 | switchToSettingsView: function () {
29 | this._switchCtx(this._settings);
30 | },
31 | isRulesView: function () {
32 | return this._isCtx(this._rules);
33 | },
34 | switchToRulesView: function () {
35 | this._switchCtx(this._rules);
36 | },
37 | isAppsView: function () {
38 | return this._isCtx(this._apps);
39 | },
40 | switchToAppsView: function () {
41 | this._switchCtx(this._apps);
42 | }
43 | };
44 | $rootScope.SIGNALS = angular.copy(signals);
45 | });
46 |
47 | myApp.service('rulesTester', function ($rootScope) {
48 | this.data = {
49 | isTesting: false,
50 | url: null,
51 | result: null
52 | };
53 |
54 | ipcRenderer.on(signals.response(signals.GENERAL.TEST_URL), (event, arg) => {
55 | this.data.result = arg;
56 | $rootScope.$apply();
57 | });
58 |
59 | this.reset = function () {
60 | this.data.isTesting = false;
61 | this.data.url = null;
62 | this.data.result = null;
63 | };
64 |
65 | this.runTest = function (rules) {
66 | this.data.isTesting = true;
67 | ipcRenderer.send(
68 | signals.request(signals.GENERAL.TEST_URL), {rules: rules, url: this.data.url}
69 | );
70 | }
71 | });
72 |
73 | myApp.service('storage', function ($rootScope, DEFAULT_CONDITION) {
74 | this.data = {
75 | rules: [],
76 | applications: [],
77 | icons: {},
78 | settings: {
79 | is_app_enabled: false,
80 | use_default: false,
81 | default_application: {}
82 | },
83 | rule: {},
84 | application: {}
85 | };
86 |
87 | this.loadRule = function (data) {
88 | if (angular.equals({}, data)) {
89 | // This is new rule
90 | data = {
91 | name: `New rule ${this.data.rules.length + 1}`,
92 | is_active: true,
93 | open_new_instance: false,
94 | open_not_foreground: false,
95 | open_fresh: false,
96 | open_args: '',
97 | application: this.data.applications.find(function (application) {
98 | return application.is_default === true;
99 | }),
100 | operator: CONST.RULE_OPERATOR.ANY,
101 | conditions: [angular.copy(DEFAULT_CONDITION)]
102 | };
103 | // We do not need any data from previous rule
104 | for (let k of Object.keys(this.data.rule)) {
105 | delete this.data.rule[k];
106 | }
107 | }
108 | // Keep a reference to the object!
109 | Object.assign(this.data.rule, angular.copy(data));
110 | };
111 |
112 | this.loadApplication = function (data) {
113 | if (angular.equals({}, data)) {
114 | data = {
115 | name: '',
116 | path: '',
117 | icns: '',
118 | display_name: '',
119 | executable: '',
120 | identifier: '',
121 | is_active: true,
122 | is_default: false
123 | };
124 | // We do not need any data from previous rule
125 | for (let k of Object.keys(this.data.application)) {
126 | delete this.data.application[k];
127 | }
128 | }
129 | // Keep a reference to the object!
130 | Object.assign(this.data.application, angular.copy(data));
131 | };
132 |
133 | // Settings: listeners
134 | ipcRenderer.on(signals.response(signals.CONFIG.READ_STATUS), (event, arg) => {
135 | this.data.settings.is_app_enabled = arg.status;
136 | $rootScope.$apply();
137 | });
138 |
139 | ipcRenderer.on(signals.response(signals.CONFIG.UPDATE_STATUS), (event, arg) => {
140 | this.data.settings.is_app_enabled = arg.status;
141 | $rootScope.$apply();
142 | });
143 |
144 | ipcRenderer.on(signals.response(signals.CONFIG.READ_IS_USE_DEFAULT), (event, arg) => {
145 | this.data.settings.use_default = arg.status;
146 | $rootScope.$apply();
147 | });
148 |
149 | ipcRenderer.on(signals.response(signals.CONFIG.UPDATE_IS_USE_DEFAULT), (event, arg) => {
150 | this.data.settings.use_default = arg.status;
151 | $rootScope.$apply();
152 | });
153 |
154 | // Settings: event emitters
155 | this.getAppStatus = function () {
156 | ipcRenderer.send(
157 | signals.request(signals.CONFIG.READ_STATUS),
158 | {query: {name: CONST.STATUS.IS_APP_ENABLED}}
159 | );
160 | };
161 |
162 | this.saveAppStatus = function () {
163 | ipcRenderer.send(
164 | signals.request(signals.CONFIG.UPDATE_STATUS),
165 | {query: {name: CONST.STATUS.IS_APP_ENABLED}, values: {status: this.data.settings.is_app_enabled}}
166 | );
167 | };
168 |
169 | this.getIsUseDefault = function () {
170 | ipcRenderer.send(
171 | signals.request(signals.CONFIG.READ_IS_USE_DEFAULT),
172 | {query: {name: CONST.STATUS.IS_USE_DEFAULT}}
173 | );
174 | };
175 |
176 | this.saveIsUseDefault = function () {
177 | ipcRenderer.send(
178 | signals.request(signals.CONFIG.UPDATE_IS_USE_DEFAULT),
179 | {query: {name: CONST.STATUS.IS_USE_DEFAULT}, values: {status: this.data.settings.use_default}}
180 | );
181 | };
182 |
183 | // Icons: listeners
184 | ipcRenderer.on(signals.response(signals.ICON.READ), (event, arg) => {
185 | let sizeToName = {
186 | 'icon_16x16.png': 'mini',
187 | 'icon_32x32.png': 'small',
188 | 'icon_32x32@2x.png': 'medium',
189 | 'icon_128x128.png': 'large'
190 | };
191 | arg.forEach(result => {
192 | if (!this.data.icons[result.application]) {
193 | this.data.icons[result.application] = {};
194 | }
195 | let alias = sizeToName[result.name];
196 | if (alias) {
197 | this.data.icons[result.application][alias] = result.content;
198 | }
199 | });
200 | $rootScope.$apply();
201 | });
202 |
203 | // Icons: event emitters
204 | this.getIcons = function (query) {
205 | ipcRenderer.send(signals.request(signals.ICON.READ), {query: query, options: {populate: false}});
206 | };
207 |
208 | // Rules: listeners
209 | ipcRenderer.on(signals.response(signals.RULE.READ), (event, arg) => {
210 | this.data.rules.splice(0, this.data.rules.length); // keep the reference to the array
211 | for (let rule of arg) {
212 | this.data.rules.push(rule);
213 | }
214 | $rootScope.$apply();
215 | });
216 |
217 | ipcRenderer.on(signals.response(signals.RULE.CREATE_ONE), (event, arg) => {
218 | this.getRules();
219 | });
220 |
221 | ipcRenderer.on(signals.response(signals.RULE.UPDATE_ONE), (event, arg) => {
222 | this.getRules();
223 | });
224 |
225 | ipcRenderer.on(signals.response(signals.RULE.DELETE_ONE), (event, arg) => {
226 | this.getRules();
227 | });
228 |
229 | ipcRenderer.on(signals.response(signals.RULE.CLEAR), (event, arg) => {
230 | this.getRules();
231 | });
232 |
233 | // Rules: event emitters
234 | this.getRules = function () {
235 | ipcRenderer.send(signals.request(signals.RULE.READ), {query: {}, options: {populate: true}});
236 | };
237 |
238 | this.createRule = function (data) {
239 | ipcRenderer.send(signals.request(signals.RULE.CREATE_ONE), {values: data || this.data.rule});
240 | };
241 |
242 | this.updateRule = function () {
243 | ipcRenderer.send(
244 | signals.request(signals.RULE.UPDATE_ONE),
245 | {query: {_id: this.data.rule._id}, values: this.data.rule}
246 | );
247 | };
248 |
249 | this.deleteRule = function (rule) {
250 | let query = rule && rule._id ? {_id: rule._id} : {_id: this.data.rule._id};
251 | ipcRenderer.send(signals.request(signals.RULE.DELETE_ONE), {query: query}
252 | );
253 | };
254 |
255 | // Applications: listeners
256 | ipcRenderer.on(signals.response(signals.APPLICATION.CREATE_ONE), (event, arg) => {
257 | this.getApplications();
258 | this.getIcons();
259 | $rootScope.$apply();
260 | });
261 |
262 | ipcRenderer.on(signals.response(signals.APPLICATION.UPDATE_DEFAULT), (event, arg) => {
263 | this.getApplications();
264 | this.getIcons();
265 | this.data.settings.default_application = arg;
266 | $rootScope.$apply();
267 | });
268 |
269 | ipcRenderer.on(signals.response(signals.APPLICATION.UPDATE_ONE), (event, arg) => {
270 | this.getApplications();
271 | });
272 |
273 | ipcRenderer.on(signals.response(signals.APPLICATION.UPDATE), (event, arg) => {
274 | this.getApplications();
275 | });
276 |
277 | ipcRenderer.on(signals.response(signals.APPLICATION.DELETE_ONE), (event, arg) => {
278 | this.getApplications();
279 | this.getRules();
280 | });
281 |
282 | ipcRenderer.on(signals.response(signals.APPLICATION.READ), (event, arg) => {
283 | this.data.applications.splice(0, this.data.applications.length); // keep the reference to the array
284 | for (let application of arg) {
285 | this.data.applications.push(application);
286 | }
287 | this.data.settings.default_application = this.data.applications.find((application) => {
288 | return application.is_default === true;
289 | });
290 | $rootScope.$apply();
291 | });
292 |
293 | ipcRenderer.on(signals.response(signals.APPLICATION.CLEAR), (event, arg) => {
294 | this.getApplications();
295 | this.getRules();
296 | });
297 |
298 | // Applications: event emitters
299 | this.getApplications = function () {
300 | ipcRenderer.send(signals.request(signals.APPLICATION.READ), {});
301 | };
302 |
303 | this.deleteApplication = function (application) {
304 | let query = application && application._id ? {_id: application._id} : {_id: this.data.application._id};
305 | ipcRenderer.send(signals.request(signals.APPLICATION.DELETE_ONE), {query: query});
306 | };
307 |
308 | this.createApplication = function () {
309 | ipcRenderer.send(signals.request(signals.APPLICATION.CREATE_ONE), {values: this.data.application});
310 | };
311 |
312 | this.updateApplication = function (application) {
313 | if (!application) {
314 | application = this.data.application
315 | }
316 | ipcRenderer.send(
317 | signals.request(signals.APPLICATION.UPDATE_ONE),
318 | {query: {_id: application._id}, values: application}
319 | );
320 | };
321 |
322 | this.setDefaultApplication = function () {
323 | ipcRenderer.send(
324 | signals.request(signals.APPLICATION.UPDATE_DEFAULT), {query: {_id: this.data.settings.default_application._id}}
325 | );
326 | };
327 |
328 | this.resetAll = function () {
329 | ipcRenderer.sendSync(signals.request(signals.CONFIG.RESET_ALL), {});
330 | };
331 | });
332 |
333 | myApp.service('actions', function () {
334 | let _canSave = false;
335 | let _canDelete = false;
336 | return {
337 | getCanSave: function () {
338 | return _canSave;
339 | },
340 | setCanSave: function (value) {
341 | _canSave = value;
342 | },
343 | getCanDelete: function () {
344 | return _canDelete;
345 | },
346 | setCanDelete: function (value) {
347 | _canDelete = value;
348 | }
349 | }
350 | });
351 |
352 | myApp.filter('capitalize', function () {
353 | return function (input) {
354 | if (input) {
355 | return (input.charAt(0).toUpperCase() + input.slice(1)).split('_').join(' ');
356 | }
357 | };
358 | });
359 |
360 | myApp.filter('filterRules', function () {
361 | return function (input, filterCriteria) {
362 | if (!filterCriteria.text && !filterCriteria.filterByStatus) {
363 | return input;
364 | }
365 | let filterText = filterCriteria.text.toLowerCase();
366 | let filterByStatus = filterCriteria.filterByStatus;
367 | return input.filter((i) => {
368 | return (i.name.toLowerCase().includes(filterText) && (filterByStatus ? i.is_active : true)) ||
369 | i.conditions.some((c) => {
370 | return c.text.toLowerCase().includes(filterText);
371 | }) && (filterByStatus ? i.is_active : true);
372 | })
373 | }
374 | });
375 |
376 | myApp.filter('filterApps', function () {
377 | return function (input, filterCriteria) {
378 | if (!filterCriteria.text && !filterCriteria.filterByStatus) {
379 | return input;
380 | }
381 | let filterText = filterCriteria.text.toLowerCase();
382 | let filterByStatus = filterCriteria.filterByStatus;
383 | return input.filter((i) => {
384 | return (i.name.toLowerCase().includes(filterText) ||
385 | i.display_name.toLowerCase().includes(filterText) ||
386 | i.executable.toLowerCase().includes(filterText) ||
387 | i.identifier.toLowerCase().includes(filterText)) &&
388 | (filterByStatus ? i.is_active : true);
389 | })
390 | }
391 | });
392 |
393 | myApp.constant('DEFAULT_CONDITION', {
394 | 'operand': 'host',
395 | 'operator': 'is',
396 | 'text': '',
397 | 'is_active': true
398 | });
399 |
400 | myApp.constant('CONDITIONS_OPTIONS', evaluators.getConditionOperandsObject());
401 |
402 | myApp.controller('MainCtrl', function ($scope, $rootScope, $q, $timeout, storage, actions, rulesTester) {
403 | // General error listener that exposes errors from node.js to AngularJS
404 | ipcRenderer.on(signals.response(signals.GENERAL.ERROR), (event, error) => {
405 | console.error(error);
406 | dialog.showErrorBox('Error', error);
407 | });
408 |
409 | $scope.rules = [];
410 | $scope.icons = {};
411 | $scope.applications = [];
412 |
413 | $scope.filterCriteria = {'text': '', 'filterByStatus': false};
414 |
415 | $scope.clearFilterCriteria = function () {
416 | $scope.filterCriteria.text = '';
417 | $scope.filterByStatus = false;
418 | };
419 |
420 | $scope.toggleFilterCriteria = function () {
421 | $scope.filterCriteria.filterByStatus = !$scope.filterCriteria.filterByStatus;
422 | };
423 |
424 | // services
425 | $scope.storage = storage;
426 | $scope.actions = actions;
427 |
428 | $scope.actions.setCanSave(false);
429 | $scope.actions.setCanDelete(false);
430 |
431 | $scope.rulesTester = rulesTester;
432 |
433 | $scope._toggleAddAppDialog = function () {
434 | if (storage.data.application._id) {
435 | return;
436 | }
437 | let opts = {
438 | title: 'Select application',
439 | defaultPath: '/Applications',
440 | filters: [
441 | {name: 'Applications', extensions: ['app']}
442 | ],
443 | properties: ['openFile']
444 | };
445 | let selectedApp = dialog.showOpenDialog(remote.getCurrentWindow(), opts);
446 | if (angular.isDefined(selectedApp) && selectedApp !== null) {
447 | if (angular.isArray(selectedApp)) {
448 | selectedApp = selectedApp[0]
449 | }
450 | let appInfo = utils.getAppInfo(selectedApp, false);
451 | storage.data.application.name = appInfo.name;
452 | storage.data.application.path = appInfo.path;
453 | storage.data.application.icns = appInfo.icns;
454 | storage.data.application.display_name = appInfo.display_name;
455 | storage.data.application.executable = appInfo.executable;
456 | storage.data.application.identifier = appInfo.identifier;
457 | storage.data.application.is_active = true;
458 | storage.data.application.is_default = false;
459 | }
460 | };
461 |
462 | $scope.toggleAddAppDialog = function () {
463 | $timeout(function () {
464 | $scope._toggleAddAppDialog();
465 | $scope.$apply();
466 | }, 500);
467 | };
468 |
469 | $scope.loadApplication = function (data) {
470 | storage.loadApplication(data || {});
471 | actions.setCanSave(false);
472 | };
473 |
474 | $scope.loadRule = function (data) {
475 | storage.loadRule(data || {});
476 | rulesTester.reset();
477 | actions.setCanSave(false);
478 | };
479 |
480 | $scope.reloadAppsView = function () {
481 | $scope.clearFilterCriteria();
482 | // Sort by date so that the newest item goes first
483 | storage.data.applications.sort(function (a, b) {
484 | return new Date(b.created_on).getTime() - new Date(a.created_on).getTime();
485 | });
486 |
487 | $scope.loadApplication({});
488 | if (storage.data.applications.length) {
489 | $scope.loadApplication(storage.data.applications[0])
490 | }
491 | $scope.ctx.switchToAppsView();
492 | };
493 |
494 | $scope.reloadRulesView = function () {
495 | $scope.clearFilterCriteria();
496 | // Sort by date so that the newest item goes first
497 | storage.data.rules.sort(function (a, b) {
498 | return new Date(b.created_on).getTime() - new Date(a.created_on).getTime();
499 | });
500 |
501 | $scope.loadRule({});
502 | if (storage.data.rules.length) {
503 | $scope.loadRule(storage.data.rules[0])
504 | }
505 | rulesTester.reset();
506 | $scope.ctx.switchToRulesView();
507 | };
508 |
509 | $scope.reloadSettingsView = function () {
510 | storage.getIsUseDefault();
511 | storage.getAppStatus();
512 | storage.getApplications();
513 | $scope.clearFilterCriteria();
514 | $scope.ctx.switchToSettingsView();
515 | };
516 |
517 | // DB operations
518 | $scope.saveRule = function () {
519 | if (storage.data.rule && storage.data.rule._id) {
520 | storage.updateRule();
521 | } else {
522 | storage.createRule();
523 | }
524 | };
525 |
526 | $scope.saveApplication = function () {
527 | if (storage.data.application && storage.data.application._id) {
528 | storage.updateApplication();
529 | } else {
530 | storage.createApplication();
531 | storage.getIcons();
532 | }
533 | };
534 |
535 | $scope.load = function () {
536 | switch (true) {
537 | case $scope.ctx.isRulesView():
538 | $scope.loadRule();
539 | break;
540 | case $scope.ctx.isAppsView():
541 | $scope.loadApplication();
542 | $scope.toggleAddAppDialog();
543 | break;
544 | default:
545 | $scope.reloadRulesView();
546 | break;
547 | }
548 | };
549 |
550 | $scope.save = function () {
551 | switch (true) {
552 | case $scope.ctx.isRulesView():
553 | $scope.saveRule();
554 | $scope.reloadRulesView();
555 | break;
556 | case $scope.ctx.isAppsView():
557 | $scope.saveApplication();
558 | $scope.reloadAppsView();
559 | break;
560 | case $scope.ctx.isSettingsView():
561 | storage.saveAppStatus();
562 | storage.saveIsUseDefault();
563 | storage.setDefaultApplication();
564 | break;
565 | default:
566 | $scope.reloadRulesView();
567 | break;
568 | }
569 | };
570 |
571 | $scope.cancel = function () {
572 | switch (true) {
573 | case $scope.ctx.isAppsView():
574 | $scope.reloadAppsView();
575 | break;
576 | case $scope.ctx.isRulesView():
577 | $scope.reloadRulesView();
578 | break;
579 | case $scope.ctx.isSettingsView():
580 | $scope.reloadSettingsView();
581 | break;
582 | default:
583 | $scope.reloadRulesView();
584 | break;
585 | }
586 | };
587 |
588 | $scope.delete = function (data) {
589 | switch (true) {
590 | case $scope.ctx.isRulesView():
591 | storage.deleteRule(data);
592 | $scope.reloadRulesView();
593 | break;
594 | case $scope.ctx.isAppsView():
595 | storage.deleteApplication(data);
596 | $scope.reloadAppsView();
597 | break;
598 | case $scope.ctx.isSettingsView():
599 | storage.resetAll();
600 | $scope.reloadSettingsView();
601 | break;
602 | }
603 | };
604 |
605 | $scope.showConfirmDelete = function () {
606 | // Currently, spectron cannot interact with dialogs so I just skip them during testing.
607 | // https://github.com/electron/spectron/issues/94
608 | if (process.env.RUNNING_IN_SPECTRON) {
609 | $scope.delete();
610 | return;
611 | }
612 | let _textContent = 'The action cannot be undone. Do you want to continue?';
613 | let _textConfirm = 'Delete';
614 | let _textTitle = 'Confirm delete';
615 | let _textDetail = null;
616 | let canDelete = true;
617 | switch (true) {
618 | case $scope.ctx.isRulesView():
619 | _textDetail = 'Note: It is possible to deactivate a rule, not delete.';
620 | break;
621 | case $scope.ctx.isAppsView():
622 | _textDetail = 'Note: All the rules that use this application will be deleted.';
623 | canDelete = storage.data.settings.default_application._id !== storage.data.application._id;
624 | if (!canDelete) {
625 | _textDetail = 'Note: You can change favorite browser in "Options" tab.';
626 | _textContent = `Cannot delete ${storage.data.application.display_name} as it is used as a favorite browser.`
627 | }
628 | break;
629 | case $scope.ctx.isSettingsView():
630 | _textConfirm = 'Reset';
631 | _textTitle = 'Confirm reset';
632 | _textDetail = 'Note: This action will clear the application settings including configured applications and rules. ' +
633 | 'Please make a backup of "~/Library/Application Support/BrowserDispatcher/databases" before you proceed.';
634 | break
635 | }
636 | let opts = {
637 | type: 'warning',
638 | title: _textTitle,
639 | message: _textContent,
640 | detail: _textDetail,
641 | buttons: canDelete ? [_textConfirm, 'Cancel'] : ['Cancel']
642 | };
643 |
644 | dialog.showMessageBox(remote.getCurrentWindow(), opts, (idx) => {
645 | if (opts.buttons.length === 2 && idx === 0) {
646 | $scope.delete();
647 | }
648 | });
649 | };
650 |
651 | $scope.showHelp = function () {
652 | ipcRenderer.send(signals.request(signals.GENERAL.SHOW_MAN_PAGE), {})
653 | };
654 |
655 | $scope.init = function () {
656 | $q.all([
657 | storage.getAppStatus(),
658 | storage.getIsUseDefault(),
659 | storage.getRules(),
660 | storage.getIcons(),
661 | storage.getApplications()
662 | ]).then(
663 | () => {
664 | $scope.rules = storage.data.rules;
665 | $scope.icons = storage.data.icons;
666 | $scope.applications = storage.data.applications;
667 |
668 | $scope.reloadRulesView();
669 | },
670 | error => {
671 | console.error(error);
672 | dialog.showErrorBox('Error', error);
673 | }
674 | );
675 | };
676 |
677 | $scope.$watch('rules', (newValue, oldValue) => {
678 | if (newValue.length && $scope.ctx.isRulesView()) {
679 | $scope.reloadRulesView();
680 | }
681 | }, true);
682 |
683 | $scope.$watch('applications', (newValue, oldValue) => {
684 | if (newValue.length && $scope.ctx.isAppsView()) {
685 | $scope.reloadAppsView();
686 | }
687 | }, true);
688 |
689 | $scope.init();
690 | });
691 |
692 | myApp.directive('removeCondition', function () {
693 | return {
694 | scope: {
695 | index: '=?'
696 | },
697 | link: function ($scope, $element) {
698 | $element.bind('click', function () {
699 | $scope.$emit('condition-remove', $scope.index);
700 | });
701 | }
702 | };
703 | });
704 |
705 | myApp.directive('toggleCondition', function () {
706 | return {
707 | scope: {
708 | index: '=?'
709 | },
710 | link: function ($scope, $element) {
711 | $element.bind('click', function () {
712 | $scope.$emit('condition-toggle', $scope.index);
713 | });
714 | }
715 | };
716 | });
717 |
718 | myApp.directive('addCondition', function () {
719 | return {
720 | scope: {
721 | index: '=?'
722 | },
723 | link: function ($scope, $element) {
724 | $element.bind('click', function () {
725 | $scope.$emit('condition-add', $scope.index);
726 | });
727 | }
728 | };
729 | });
730 |
731 | myApp.directive('editCondition', function () {
732 | return {
733 | restrict: 'E',
734 | scope: {
735 | condition: '=?condition',
736 | index: '=?index'
737 | },
738 | replace: true,
739 | transclude: true,
740 | templateUrl: './src/app/templates/edit_condition.html',
741 |
742 | controller: function ($scope, CONDITIONS_OPTIONS) {
743 | let conditionOptions = angular.copy(CONDITIONS_OPTIONS);
744 | $scope.operands = Object.keys(conditionOptions);
745 | $scope.operators = conditionOptions[$scope.condition.operand];
746 |
747 | $scope.$watch('condition.operand', function (newValue, oldValue) {
748 | if (newValue === oldValue) {
749 | return;
750 | }
751 | let operators = conditionOptions[$scope.condition.operand];
752 | if (!angular.equals($scope.operators, operators)) {
753 | $scope.operators = conditionOptions[$scope.condition.operand];
754 | $scope.condition.operator = $scope.operators[0];
755 | }
756 | });
757 | }
758 | }
759 | });
760 |
761 | myApp.controller('SettingsCtrl', function ($scope, storage, actions) {
762 | // a tiny proxy Controller for storage objects
763 | $scope.settings = storage.data.settings;
764 | $scope.applications = storage.data.applications;
765 | actions.setCanSave(true);
766 | actions.setCanDelete(true);
767 | });
768 |
769 | myApp.controller('RuleCtrl', function ($scope, DEFAULT_CONDITION, storage, actions) {
770 | $scope.rule = storage.data.rule;
771 | $scope.applications = storage.data.applications;
772 |
773 | actions.setCanSave(false);
774 | actions.setCanDelete(false);
775 |
776 | $scope.toggleActive = function () {
777 | $scope.editRule.$dirty = true;
778 | $scope.rule.is_active = !$scope.rule.is_active;
779 | };
780 |
781 | $scope.isActive = function () {
782 | return (angular.isDefined($scope.rule) && $scope.rule && $scope.rule.is_active);
783 | };
784 |
785 | $scope.hasConditions = function () {
786 | return $scope.rule.conditions.length > 0;
787 | };
788 |
789 | $scope.$on('condition-add', function (e, index) {
790 | $scope.editRule.$dirty = true;
791 | $scope.rule.conditions.splice(Number.parseInt(index) + 1, 0, angular.copy(DEFAULT_CONDITION));
792 | $scope.$apply();
793 | });
794 |
795 | $scope.$on('condition-remove', function (e, index) {
796 | if ($scope.rule.conditions.length > 1) {
797 | $scope.editRule.$dirty = true;
798 | $scope.rule.conditions.splice(Number.parseInt(index), 1);
799 | $scope.$apply();
800 | }
801 | });
802 |
803 | $scope.$on('condition-toggle', function (e, index) {
804 | if ($scope.rule.conditions.length > 1) {
805 | $scope.editRule.$dirty = true;
806 | $scope.rule.conditions[index].is_active = !$scope.rule.conditions[index].is_active;
807 | $scope.$apply();
808 | }
809 | });
810 |
811 | $scope.$watch('rule', (newValue, oldValue) => {
812 | if (newValue._id !== oldValue._id) {
813 | $scope.editRule.$dirty = false;
814 | }
815 | actions.setCanDelete(angular.isDefined($scope.rule._id));
816 | if (newValue !== oldValue && $scope.editRule.$dirty) {
817 | actions.setCanSave([
818 | newValue.name,
819 | newValue.conditions && newValue.conditions.every(function (c) { return c.text; })
820 | ].every(function (attr) { return attr; }));
821 | }
822 | }, true);
823 | });
824 |
825 | myApp.controller('ApplicationCtrl', function ($scope, $timeout, storage, actions) {
826 | $scope.application = storage.data.application;
827 |
828 | actions.setCanSave(false);
829 | actions.setCanDelete(false);
830 |
831 | $scope.toggleActive = function () {
832 | $scope.editApp.$dirty = true;
833 | $scope.application.is_active = !$scope.application.is_active;
834 | };
835 |
836 | $scope.isActive = function () {
837 | return (angular.isDefined($scope.application) && $scope.application.is_active);
838 | };
839 |
840 | $scope.$watch('application', (newValue, oldValue) => {
841 | if (newValue._id !== oldValue._id) {
842 | $scope.editApp.$dirty = false;
843 | }
844 | actions.setCanDelete(angular.isDefined($scope.application._id));
845 | if (newValue !== oldValue && $scope.editApp.$dirty) {
846 | actions.setCanSave([
847 | newValue.name,
848 | newValue.executable,
849 | newValue.identifier,
850 | newValue.display_name
851 | ].every(function (attr) { return attr; }));
852 | }
853 | }, true)
854 | });
855 |
--------------------------------------------------------------------------------
/src/app/app2.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const electron = require('electron');
4 | const {remote, ipcRenderer} = require('electron');
5 | const {dialog} = require('electron').remote;
6 | const signals = require('./src/app/js/signals');
7 |
8 | const myApp2 = angular.module('myApp2', ['ngSanitize', 'ngMaterial', 'ngMessages']);
9 |
10 | myApp2.controller('MainCtrl2', function ($scope) {
11 | // General error listener that exposes errors from node.js to AngularJS
12 | ipcRenderer.on(signals.response(signals.GENERAL.ERROR), (event, error) => {
13 | console.error(error);
14 | dialog.showErrorBox('Error', error)
15 | });
16 |
17 | $scope.applications = [];
18 | $scope.icons = [];
19 | $scope.urls = [];
20 |
21 | $scope.win = null;
22 |
23 | // Load URL that should be opened
24 | ipcRenderer.on(signals.response(signals.GENERAL.LOAD_URL), (event, arg) => {
25 | $scope.urls = [arg];
26 | $scope.showWin();
27 | });
28 |
29 | // Icons: listeners
30 | ipcRenderer.on(signals.response(signals.ICON.READ), (event, arg) => {
31 | let sizeToName = {
32 | 'icon_16x16.png': 'mini',
33 | 'icon_32x32.png': 'small',
34 | 'icon_32x32@2x.png': 'medium',
35 | 'icon_128x128.png': 'large'
36 | };
37 | arg.forEach(result => {
38 | if (!$scope.icons[result.application]) {
39 | $scope.icons[result.application] = {};
40 | }
41 | let alias = sizeToName[result.name];
42 | if (alias) {
43 | $scope.icons[result.application][alias] = result.content;
44 | }
45 | });
46 | $scope.$apply();
47 | });
48 |
49 | // Icons: event emitters
50 | $scope.getIcons = function () {
51 | ipcRenderer.send(signals.request(signals.ICON.READ), {query: {}, options: {populate: false}});
52 | };
53 |
54 | ipcRenderer.on(signals.response(signals.APPLICATION.READ), (event, arg) => {
55 | $scope.applications.splice(0, $scope.applications.length); // keep the reference to the array
56 | for (let application of arg) {
57 | $scope.applications.push(application);
58 | }
59 | $scope.$apply();
60 | });
61 |
62 | $scope.getApplications = function () {
63 | ipcRenderer.send(signals.request(signals.APPLICATION.READ), {});
64 | };
65 |
66 | $scope.openApplication = function (application) {
67 | ipcRenderer.send(
68 | signals.request(signals.GENERAL.OPEN_URL),
69 | {application: application, url: $scope.urls[0]}
70 | );
71 | };
72 |
73 | $scope.getApplications();
74 | $scope.getIcons();
75 |
76 | $scope.hideWin = function () {
77 | $scope.win = $scope.win || remote.getCurrentWindow();
78 | if ($scope.win && $scope.win.isVisible()) {
79 | $scope.win.hide();
80 | }
81 | };
82 |
83 | $scope.getWin = function () {
84 | $scope.win = remote.getCurrentWindow();
85 | let {width, height} = electron.screen.getPrimaryDisplay().workArea;
86 | let {x, y} = electron.screen.getCursorScreenPoint();
87 | // width +50 for left & right padding
88 | let w = Math.min($scope.applications.length * 42 + 50, 800);
89 | let h = 100;
90 | let x2 = x - Math.round(w / 2);
91 | let y2 = y - h;
92 |
93 | // make sure that the pop-up window is within the screen area
94 | x2 = Math.max(x2, 0);
95 | x2 = Math.min((x2 + w), width) - w;
96 |
97 | y2 = Math.max(y2, 0);
98 | y2 = Math.min((y2 + h), height) - h;
99 |
100 | $scope.win.setSize(w, h);
101 | $scope.win.setPosition(x2, y2);
102 | };
103 |
104 | $scope.showWin = function () {
105 | if (!$scope.win) {
106 | $scope.getWin();
107 | }
108 | if (!$scope.win.isVisible()) {
109 | $scope.win.show();
110 | $scope.win.focus();
111 | }
112 | };
113 |
114 | $scope.$watch('applications', () => {
115 | $scope.getWin();
116 | }, true);
117 | });
118 |
--------------------------------------------------------------------------------
/src/app/css/app.css:
--------------------------------------------------------------------------------
1 | :focus {
2 | outline: none;
3 | }
4 |
5 | * {
6 | cursor: default;
7 | -webkit-user-drag: none;
8 | -webkit-user-select: none;
9 | -webkit-box-sizing: border-box;
10 | box-sizing: border-box;
11 | }
12 |
13 | input[type="file"]{
14 | font-size: 0;
15 | }
16 |
17 | input[type="file"]::-webkit-file-upload-button{
18 | font-size: 14px; /*normal size*/
19 | }
20 |
21 | .bd-form-control {
22 | margin-left: 4px;
23 | display: inline;
24 | width: 90%;
25 | min-height: 12px;
26 | padding: 5px 10px;
27 | font-size: inherit;
28 | line-height: normal;
29 | border: 1px solid #ddd;
30 | border-radius: 4px;
31 | outline: none;
32 | }
33 |
34 | .bd-padded {
35 | margin-top: 10px;
36 | margin-bottom: 10px;
37 | }
38 |
39 | .bd-select-input {
40 | display: inline;
41 | width: 25%;
42 | }
43 |
44 | .bd-text-input {
45 | padding-left: 4px;
46 | padding-right: 4px;
47 | display: inline;
48 | line-height: 130%;
49 | background-color: #fff;
50 | border: 1px solid #ddd;
51 | border-radius: 3px;
52 | outline: none;
53 | }
54 |
55 | .bd-text-input:focus {
56 | outline: -webkit-focus-ring-color auto 3px;
57 | }
58 |
59 | .bd-toolbar-actions {
60 | -webkit-app-region: none;
61 | }
62 |
63 | .box {
64 | width: 100%;
65 | }
66 |
67 | .bd-button, .bd-button-group {
68 | vertical-align: inherit;
69 | }
70 |
71 | .bd-button-mini {
72 | padding: 1px 6px;
73 | }
74 |
75 | .bd-pane-sm {
76 | max-width: 30%;
77 | min-width: 200px
78 | }
79 |
80 | .bd-center {
81 | display: flex;
82 | justify-content: center;
83 | align-items: center;
84 | }
85 |
86 | .bd-center-end {
87 | display:flex;
88 | justify-content:flex-end;
89 | align-items:center;
90 | }
91 |
92 | .bd-center-start {
93 | display:flex;
94 | justify-content:flex-start;
95 | align-items:center;
96 | }
97 |
98 |
99 | span.bd-shift-span {
100 | margin-right: 2px;
101 | }
102 |
103 | button.bd-asymmetric {
104 | border-bottom-left-radius: 0;
105 | border-top-left-radius: 0;
106 | margin-left: -2px;
107 | padding-right: 12px
108 | }
109 |
110 | .bd-button-icon, .bd-button-icon:active {
111 | background: transparent none;
112 | border: none;
113 | box-shadow: none;
114 | }
115 |
116 | #toolbarTop {
117 | box-shadow: none;
118 | }
--------------------------------------------------------------------------------
/src/app/css/app2.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | font-family: 'Roboto', sans-serif;
3 | font-size: 14px;
4 | height: 100%;
5 | margin: 0;
6 | padding: 0;
7 | overflow-x: hidden;
8 | background-color: rgba(115,116,117,0.8);
9 | }
10 |
11 | md-toolbar {
12 | margin-top: 10px;
13 | background-color: transparent !important;
14 | }
15 |
16 | .md-toolbar-tools {
17 | padding-left: 35px;
18 | padding-right: 35px;
19 | }
20 |
21 | .md-button.md-icon-button {
22 | margin: 0;
23 | height: 60px;
24 | min-width: 0;
25 | line-height: 24px;
26 | padding-left: 0;
27 | padding-right: 0;
28 | width: 60px;
29 | border-radius: 50%;
30 | cursor: default;
31 | }
32 |
33 | .md-chips {
34 | font-size: 12px;
35 | padding-bottom: 0;
36 | box-shadow: none !important;
37 | }
38 |
39 | .md-chips md-chip {
40 | height: 20px;
41 | line-height: 18px;
42 | background-color: rgba(115,116,117,0.9);
43 | color: rgb(255,255,255);
44 | box-shadow: none !important;
45 | }
46 |
47 | md-icon {
48 | margin-left: 15px;
49 | margin-right: -20px;
50 | margin-top: 0;
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/css/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Material Icons';
3 | font-style: normal;
4 | font-weight: 400;
5 | src: local('Material Icons'), local('MaterialIcons-Regular'), url('fonts/MaterialIcons.woff') format('woff');
6 | }
7 |
8 | .material-icons {
9 | font-family: 'Material Icons';
10 | font-weight: normal;
11 | font-style: normal;
12 | font-size: 24px;
13 | line-height: 1;
14 | letter-spacing: normal;
15 | text-transform: none;
16 | display: inline-block;
17 | white-space: nowrap;
18 | word-wrap: normal;
19 | direction: ltr;
20 | text-rendering: optimizeLegibility;
21 | -webkit-font-smoothing: antialiased;
22 | }
23 |
24 |
25 | @font-face {
26 | font-family: 'Roboto';
27 | font-style: normal;
28 | font-weight: 300;
29 | src: local('Roboto Light'), local('Roboto-Light'), url('fonts/Roboto-Light.woff') format('woff');
30 | }
31 | @font-face {
32 | font-family: 'Roboto';
33 | font-style: normal;
34 | font-weight: 400;
35 | src: local('Roboto'), local('Roboto-Regular'), url('fonts/Roboto-Regular.woff') format('woff');
36 | }
37 | @font-face {
38 | font-family: 'Roboto';
39 | font-style: normal;
40 | font-weight: 500;
41 | src: local('Roboto Medium'), local('Roboto-Medium'), url('fonts/Roboto-Medium.woff') format('woff');
42 | }
43 | @font-face {
44 | font-family: 'Roboto';
45 | font-style: normal;
46 | font-weight: 700;
47 | src: local('Roboto Bold'), local('Roboto-Bold'), url('fonts/Roboto-Bold.woff') format('woff');
48 | }
49 | @font-face {
50 | font-family: 'Roboto';
51 | font-style: italic;
52 | font-weight: 400;
53 | src: local('Roboto Italic'), local('Roboto-Italic'), url('fonts/Roboto-Italic.woff') format('woff');
54 | }
55 |
56 | .roboto {
57 | font-family: 'Roboto', sans-serif;
58 | font-weight: normal;
59 | font-style: normal;
60 | font-size: 24px;
61 | }
--------------------------------------------------------------------------------
/src/app/css/fonts/MaterialIcons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/src/app/css/fonts/MaterialIcons.woff
--------------------------------------------------------------------------------
/src/app/css/fonts/Roboto-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/src/app/css/fonts/Roboto-Bold.woff
--------------------------------------------------------------------------------
/src/app/css/fonts/Roboto-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/src/app/css/fonts/Roboto-Italic.woff
--------------------------------------------------------------------------------
/src/app/css/fonts/Roboto-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/src/app/css/fonts/Roboto-Light.woff
--------------------------------------------------------------------------------
/src/app/css/fonts/Roboto-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/src/app/css/fonts/Roboto-Medium.woff
--------------------------------------------------------------------------------
/src/app/css/fonts/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/src/app/css/fonts/Roboto-Regular.woff
--------------------------------------------------------------------------------
/src/app/css/icons.css:
--------------------------------------------------------------------------------
1 | /* Rules for sizing the icon. */
2 | .material-icons.md-12 {
3 | font-size: 12px;
4 | height: 12px;
5 | width: 12px;
6 | min-height: 12px;
7 | min-width: 12px;
8 | }
9 |
10 | .material-icons.md-14 {
11 | font-size: 14px;
12 | height: 14px;
13 | width: 14px;
14 | min-height: 14px;
15 | min-width: 14px;
16 | }
17 |
18 | .material-icons.md-16 {
19 | font-size: 16px;
20 | height: 16px;
21 | width: 16px;
22 | min-height: 16px;
23 | min-width: 16px;
24 | }
25 |
26 | .material-icons.md-18 {
27 | font-size: 18px;
28 | height: 18px;
29 | width: 18px;
30 | min-height: 18px;
31 | min-width: 18px;
32 |
33 | }
34 |
35 | .material-icons.md-24 {
36 | font-size: 24px;
37 | height: 24px;
38 | width: 24px;
39 | min-height: 24px;
40 | min-width: 24px;
41 | }
42 |
43 | .material-icons.md-36 {
44 | font-size: 36px;
45 | height: 36px;
46 | width: 36px;
47 | min-height: 36px;
48 | min-width: 36px;
49 | }
50 |
51 | .material-icons.md-48 {
52 | font-size: 48px;
53 | height: 48px;
54 | width: 48px;
55 | min-height: 48px;
56 | min-width: 48px;
57 | }
58 |
59 | /* Rules for using icons as black on a light background. */
60 | .material-icons.md-danger { color: rgba(255, 17, 14, 0.84); }
61 | .material-icons.md-danger.md-inactive { color: rgba(255, 17, 14, 0.54); }
62 |
63 | md-icon.md-danger { color: rgba(255, 17, 14, 0.84); }
64 | md-icon.md-danger.md-inactive { color: rgba(255, 17, 14, 0.54); }
65 |
66 | .material-icons.md-dark { color: rgba(0, 0, 0, 0.54); }
67 | .material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
68 |
69 | /* Rules for using icons as white on a dark background. */
70 | .material-icons.md-light { color: rgba(255, 255, 255, 1); }
71 | .material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
72 |
73 | /* Rules for using icons as black on a light background. */
74 | md-icon.md-dark { color: rgba(0, 0, 0, 0.54); }
75 | md-icon.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
76 |
77 | /* Rules for using icons as white on a dark background. */
78 | md-icon.md-light { color: rgba(255, 255, 255, 1); }
79 | md-icon.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
80 |
--------------------------------------------------------------------------------
/src/app/images/tray_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/src/app/images/tray_icon.png
--------------------------------------------------------------------------------
/src/app/js/constants.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const COMMON = {
4 | APP_NAME: 'BrowserDispatcher'
5 | };
6 |
7 | const CONDITION_OPERATOR = {
8 | IS: 'is',
9 | IS_NOT: 'is_not',
10 | CONTAINS: 'contains',
11 | NOT_CONTAINS: 'not_contains',
12 | STARTS_WITH: 'starts_with',
13 | NOT_STARTS_WITH: 'not_starts_with',
14 | ENDS_WITH: 'ends_with',
15 | NOT_ENDS_WITH: 'not_ends_with',
16 | REGEX: 'regular_expression'
17 | };
18 |
19 | const CONDITION_OPERAND = {
20 | URL: 'url',
21 | APP: 'app',
22 | HOST: 'host',
23 | SCHEME: 'scheme',
24 | PATH: 'path',
25 | PORT: 'port',
26 | EXTENSION: 'extension'
27 | };
28 |
29 | const RULE_OPERATOR = {
30 | ALL: 'all',
31 | ANY: 'any'
32 | };
33 |
34 | const STATUS = {
35 | IS_APP_ENABLED: 'is_app_enabled',
36 | IS_DEV_MODE: 'is_dev_mode',
37 | IS_USE_DEFAULT: 'is_use_default'
38 | };
39 |
40 | module.exports = {
41 | 'COMMON': COMMON,
42 | 'RULE_OPERATOR': RULE_OPERATOR,
43 | 'CONDITION_OPERATOR': CONDITION_OPERATOR,
44 | 'CONDITION_OPERAND': CONDITION_OPERAND,
45 | 'STATUS': STATUS
46 | };
47 |
--------------------------------------------------------------------------------
/src/app/js/docs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const Promise = require('bluebird');
5 |
6 | const {Document, EmbeddedDocument, connect} = require('camo');
7 |
8 | const utils = require('./utils');
9 | const CONST = require('./constants');
10 | const {getLogger} = require('./logger');
11 |
12 | class Condition extends EmbeddedDocument {
13 | constructor () {
14 | super();
15 |
16 | this.text = {
17 | type: String,
18 | required: true
19 | };
20 | this.is_active = {
21 | type: Boolean,
22 | default: true
23 | };
24 | this.operand = {
25 | type: String,
26 | choices: utils.objectValues(CONST.CONDITION_OPERAND),
27 | required: true
28 | };
29 | this.operator = {
30 | type: String,
31 | choices: utils.objectValues(CONST.CONDITION_OPERATOR),
32 | required: true
33 | };
34 | this.created_on = {
35 | type: Date,
36 | default: Date.now
37 | };
38 | };
39 |
40 | preValidate () {
41 | this.created_on = new Date();
42 | return this;
43 | };
44 |
45 | static collectionName () {
46 | return 'conditions';
47 | };
48 | }
49 |
50 | class Icon extends Document {
51 | constructor () {
52 | super();
53 |
54 | this.name = {
55 | required: true,
56 | type: String
57 | };
58 | this.content = {
59 | required: true,
60 | type: String
61 | };
62 | this.application = {
63 | required: true,
64 | type: Application
65 | };
66 | this.created_on = {
67 | type: Date,
68 | default: Date.now
69 | };
70 | }
71 |
72 | preValidate () {
73 | this.created_on = new Date();
74 | if (Buffer.isBuffer(this.content)) {
75 | this.content = this.content.toString('base64');
76 | }
77 | return this;
78 | };
79 |
80 | static collectionName () {
81 | return 'icons';
82 | };
83 | }
84 |
85 | class Application extends Document {
86 | constructor () {
87 | super();
88 |
89 | this.name = {
90 | unique: true,
91 | required: true,
92 | type: String
93 | };
94 | this.path = {
95 | unique: true,
96 | required: true,
97 | type: String
98 | };
99 | this.icns = {
100 | unique: true,
101 | required: true,
102 | type: String
103 | };
104 | this.display_name = {
105 | required: true,
106 | type: String
107 | };
108 | this.executable = {
109 | required: true,
110 | type: String
111 | };
112 | this.identifier = {
113 | unique: true,
114 | required: true,
115 | type: String
116 | };
117 | this.is_default = {
118 | type: Boolean,
119 | default: false
120 | };
121 | this.is_active = {
122 | type: Boolean,
123 | default: true
124 | };
125 | this.created_on = {
126 | type: Date,
127 | default: Date.now
128 | };
129 | };
130 |
131 | preValidate () {
132 | this.created_on = new Date();
133 | return this;
134 | }
135 |
136 | static collectionName () {
137 | return 'applications';
138 | };
139 | }
140 |
141 | class Rule extends Document {
142 | constructor () {
143 | super();
144 |
145 | this.name = {
146 | unique: true,
147 | required: true,
148 | type: String
149 | };
150 | this.is_active = {
151 | type: Boolean,
152 | default: true
153 | };
154 | this.operator = {
155 | type: String,
156 | choices: utils.objectValues(CONST.RULE_OPERATOR),
157 | required: true
158 | };
159 | this.conditions = [Condition];
160 | this.application = {
161 | required: true,
162 | type: Application
163 | };
164 | // "-n" option
165 | // Open a new instance of the application(s) even if one is already running.
166 | this.open_new_instance = {
167 | type: Boolean,
168 | default: false
169 | };
170 | // "-g" option
171 | // Do not bring the application to the foreground.
172 | this.open_not_foreground = {
173 | type: Boolean,
174 | default: false
175 | };
176 | // "-F" option.
177 | // Opens the application "fresh," that is, without restoring windows.
178 | // Saved persistent state is lost, except for Untitled documents.
179 | this.open_fresh = {
180 | type: Boolean,
181 | default: false
182 | };
183 | // "−b" option
184 | // Use bundle identifier of the application to use when opening the file/uri
185 | this.use_app_executable = {
186 | type: Boolean,
187 | default: false
188 | };
189 | // "--args" option.
190 | // All remaining arguments are passed to the opened application in the argv parameter to main().
191 | // These arguments are not opened or interpreted by the open tool.
192 | this.open_args = {
193 | type: String,
194 | required: false
195 | };
196 | this.created_on = {
197 | type: Date,
198 | default: Date.now
199 | };
200 | }
201 |
202 | preValidate () {
203 | this.created_on = new Date();
204 | if (typeof this.application._id !== 'undefined') {
205 | this.application = this.application._id;
206 | }
207 | return this;
208 | }
209 |
210 | static collectionName () {
211 | return 'rules';
212 | };
213 | }
214 |
215 | class Preferences extends Document {
216 | constructor () {
217 | super();
218 |
219 | this.name = {
220 | unique: true,
221 | required: true,
222 | type: String
223 | };
224 | this.status = {
225 | default: true,
226 | type: Boolean
227 | };
228 | this.created_on = {
229 | type: Date,
230 | default: Date.now
231 | };
232 | };
233 |
234 | static init (logger) {
235 | let initData = {name: CONST.STATUS.IS_APP_ENABLED};
236 | return this.findOne(initData).then(
237 | foundData => {
238 | if (foundData) {
239 | logger.info(`Skipping initialization of "${this.name}"`);
240 | return foundData;
241 | } else {
242 | let appStatus = this.create(initData);
243 | let devStatus = this.create({name: CONST.STATUS.IS_DEV_MODE, status: false});
244 | let useDefault = this.create({name: CONST.STATUS.IS_USE_DEFAULT, status: false});
245 | Promise.all([appStatus.save(), devStatus.save(), useDefault.save()]).then(
246 | result => {
247 | logger.info(`Successfully initialized "${this.name}"`);
248 | return result;
249 | },
250 | error => {
251 | logger.error(`Failed to initialize "${this.name}". Error: ${error}`);
252 | return Promise.reject(error);
253 | }
254 | );
255 | }
256 | },
257 | error => {
258 | logger.error(`Failed to initialize "${this.name}". Error: ${error}`);
259 | return Promise.reject(error);
260 | }
261 | );
262 | };
263 |
264 | static isAppEnabled () {
265 | return this.findOne({name: CONST.STATUS.IS_APP_ENABLED});
266 | };
267 |
268 | static enableApp () {
269 | return this.findOneAndUpdate({name: CONST.STATUS.IS_APP_ENABLED}, {status: true}, {});
270 | };
271 |
272 | static disableApp () {
273 | return this.findOneAndUpdate({name: CONST.STATUS.IS_APP_ENABLED}, {status: false}, {});
274 | };
275 |
276 | static toggleAppStatus () {
277 | return this.isAppEnabled().then(
278 | result => {
279 | if (result.status) {
280 | return this.disableApp();
281 | } else {
282 | return this.enableApp();
283 | }
284 | },
285 | error => {
286 | return Promise.reject(error);
287 | }
288 | );
289 | };
290 |
291 | preValidate () {
292 | this.created_on = new Date();
293 | return this;
294 | };
295 |
296 | static collectionName () {
297 | return 'settings';
298 | }
299 | }
300 |
301 | class CamoWrapper {
302 | constructor (basePath) {
303 | this._db_uri = `nedb://${path.join(basePath, 'databases/')}`;
304 | this._logger = getLogger(path.join(basePath, `${CONST.COMMON.APP_NAME}.log`));
305 | this._db = null;
306 |
307 | this.connect().then(() => {
308 | this.prefs = Preferences;
309 | this.icon = Icon;
310 | this.application = Application;
311 | this.condition = Condition;
312 | this.rule = Rule;
313 | });
314 | };
315 |
316 | connect () {
317 | return connect(this._db_uri).then(
318 | db => {
319 | this._logger.info('Connected to database');
320 | this._db = db;
321 | return this._db;
322 | },
323 | error => {
324 | this._logger.error(error);
325 | return Promise.reject(error);
326 | }
327 | );
328 | };
329 |
330 | init () {
331 | return Promise.all([
332 | this.prefs.init(this._logger)
333 | ]);
334 | };
335 | }
336 |
337 | module.exports = {
338 | 'Icon': Icon,
339 | 'Condition': Condition,
340 | 'Rule': Rule,
341 | 'Application': Application,
342 | 'Preferences': Preferences,
343 | 'CamoWrapper': CamoWrapper
344 | };
345 |
--------------------------------------------------------------------------------
/src/app/js/evaluators.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const url = require('url');
4 | const utils = require('./utils');
5 | const CONST = require('./constants');
6 |
7 | class BaseOperator {
8 | constructor (val) {
9 | this.val = val.trim().toLowerCase();
10 | }
11 |
12 | _evaluate (val) {
13 | return false;
14 | }
15 |
16 | evaluate (val) {
17 | if (val !== null && val !== undefined) {
18 | return this._evaluate(val.trim().toLowerCase());
19 | }
20 | return false;
21 | }
22 |
23 | static get uid () {
24 | return null;
25 | }
26 | }
27 |
28 | class OperatorIs extends BaseOperator {
29 | _evaluate (val) {
30 | return this.val === val;
31 | }
32 | static get uid () {
33 | return CONST.CONDITION_OPERATOR.IS;
34 | }
35 | }
36 |
37 | class OperatorIsNot extends BaseOperator {
38 | _evaluate (val) {
39 | return this.val !== val;
40 | }
41 | static get uid () {
42 | return CONST.CONDITION_OPERATOR.IS_NOT;
43 | }
44 | }
45 |
46 | class OperatorContains extends BaseOperator {
47 | _evaluate (val) {
48 | return val.indexOf(this.val) > -1;
49 | }
50 | static get uid () {
51 | return CONST.CONDITION_OPERATOR.CONTAINS;
52 | }
53 | }
54 |
55 | class OperatorNotContains extends BaseOperator {
56 | _evaluate (val) {
57 | return val.indexOf(this.val) === -1;
58 | }
59 | static get uid () {
60 | return CONST.CONDITION_OPERATOR.NOT_CONTAINS;
61 | }
62 | }
63 |
64 | class OperatorStartsWith extends BaseOperator {
65 | _evaluate (val) {
66 | return new RegExp('^' + this.val, 'i').test(val);
67 | }
68 | static get uid () {
69 | return CONST.CONDITION_OPERATOR.STARTS_WITH;
70 | }
71 | }
72 |
73 | class OperatorNotStartsWith extends BaseOperator {
74 | _evaluate (val) {
75 | return !(new RegExp('^' + this.val, 'i').test(val));
76 | }
77 | static get uid () {
78 | return CONST.CONDITION_OPERATOR.NOT_STARTS_WITH;
79 | }
80 | }
81 |
82 | class OperatorEndsWith extends BaseOperator {
83 | _evaluate (val) {
84 | return new RegExp(this.val + '$', 'i').test(val);
85 | }
86 | static get uid () {
87 | return CONST.CONDITION_OPERATOR.ENDS_WITH;
88 | }
89 | }
90 |
91 | class OperatorNotEndsWith extends BaseOperator {
92 | _evaluate (val) {
93 | return !(new RegExp(this.val + '$', 'i').test(val));
94 | }
95 | static get uid () {
96 | return CONST.CONDITION_OPERATOR.NOT_ENDS_WITH;
97 | }
98 | }
99 |
100 | class OperatorRegexp extends BaseOperator {
101 | _evaluate (val) {
102 | return new RegExp(this.val, 'i').test(val);
103 | }
104 | static get uid () {
105 | return CONST.CONDITION_OPERATOR.REGEX;
106 | }
107 | }
108 |
109 | function getConditionOperators () {
110 | let d = {};
111 | for (let op of [OperatorIs, OperatorContains, OperatorStartsWith, OperatorEndsWith,
112 | OperatorIsNot, OperatorNotContains, OperatorNotStartsWith, OperatorNotEndsWith,
113 | OperatorRegexp]) {
114 | d[op.uid] = op;
115 | }
116 | return d;
117 | }
118 |
119 | class OperandApp {
120 | static get uid () {
121 | return CONST.CONDITION_OPERAND.APP;
122 | }
123 |
124 | static get supportedOperators () {
125 | return [OperatorIs.uid, OperatorIsNot.uid];
126 | }
127 | }
128 |
129 | class OperandHost {
130 | static get uid () {
131 | return CONST.CONDITION_OPERAND.HOST;
132 | }
133 |
134 | static get supportedOperators () {
135 | return [
136 | OperatorIs.uid, OperatorIsNot.uid, OperatorContains.uid,
137 | OperatorNotContains.uid, OperatorStartsWith.uid, OperatorNotStartsWith.uid,
138 | OperatorEndsWith.uid, OperatorNotEndsWith.uid
139 | ];
140 | }
141 | }
142 |
143 | class OperandScheme {
144 | static get uid () {
145 | return CONST.CONDITION_OPERAND.SCHEME;
146 | }
147 |
148 | static get supportedOperators () {
149 | return [OperatorIs.uid, OperatorIsNot.uid];
150 | }
151 | }
152 |
153 | class OperandPath {
154 | static get uid () {
155 | return CONST.CONDITION_OPERAND.PATH;
156 | }
157 |
158 | static get supportedOperators () {
159 | return [
160 | OperatorIs.uid, OperatorIsNot.uid, OperatorContains.uid,
161 | OperatorNotContains.uid, OperatorStartsWith.uid, OperatorNotStartsWith.uid,
162 | OperatorEndsWith.uid, OperatorNotEndsWith.uid
163 | ];
164 | }
165 | }
166 |
167 | class OperandPort {
168 | static get uid () {
169 | return CONST.CONDITION_OPERAND.PORT;
170 | }
171 |
172 | static get supportedOperators () {
173 | return [OperatorIs.uid, OperatorIsNot.uid];
174 | }
175 | }
176 |
177 | class OperandExtension {
178 | static get uid () {
179 | return CONST.CONDITION_OPERAND.EXTENSION;
180 | }
181 |
182 | static get supportedOperators () {
183 | return [OperatorIs.uid, OperatorIsNot.uid];
184 | }
185 | }
186 |
187 | class OperandUrl {
188 | static get uid () {
189 | return CONST.CONDITION_OPERAND.URL;
190 | }
191 |
192 | static get supportedOperators () {
193 | return [OperatorRegexp.uid];
194 | }
195 | }
196 |
197 | function getConditionOperands () {
198 | let d = {};
199 | // d[OperandApp.uid] = OperandApp;
200 | d[OperandHost.uid] = OperandHost;
201 | d[OperandScheme.uid] = OperandScheme;
202 | d[OperandPath.uid] = OperandPath;
203 | d[OperandPort.uid] = OperandPort;
204 | d[OperandUrl.uid] = OperandUrl;
205 | // d[OperandExtension.uid] = OperandExtension;
206 | return d;
207 | }
208 |
209 | function getConditionOperandsObject () {
210 | let operands = {};
211 |
212 | for (let [key, value] of utils.entries(getConditionOperands())) {
213 | operands[key] = value.supportedOperators;
214 | }
215 | return operands;
216 | }
217 |
218 | class RuleEvaluator {
219 | constructor (rule) {
220 | this.rule = rule;
221 | }
222 |
223 | _evaluateCondition (conditionToEvaluate, objectToEvaluate) {
224 | let operand = getConditionOperands()[conditionToEvaluate.operand];
225 | let CondClass = getConditionOperators()[conditionToEvaluate.operator];
226 | let cond = new CondClass(conditionToEvaluate.text);
227 | let value = objectToEvaluate[operand.uid];
228 | return cond.evaluate(value);
229 | }
230 |
231 | _any (objectToEvaluate) {
232 | let result = false;
233 | for (let condition of this.rule.conditions) {
234 | result = this._evaluateCondition(condition, objectToEvaluate);
235 | if (result) {
236 | break;
237 | }
238 | }
239 | return result;
240 | }
241 |
242 | _all (objectToEvaluate) {
243 | let result = true;
244 | for (let condition of this.rule.conditions) {
245 | result = this._evaluateCondition(condition, objectToEvaluate);
246 | if (!result) {
247 | break;
248 | }
249 | }
250 | return result;
251 | }
252 |
253 | evaluate (objectToEvaluate) {
254 | if (this.rule.operator === CONST.RULE_OPERATOR.ANY) {
255 | return this._any(objectToEvaluate);
256 | }
257 | return this._all(objectToEvaluate);
258 | }
259 | }
260 |
261 | function crateObjectToEvaluate (value, event) {
262 | let parsedUrl = url.parse(value);
263 | let result = {};
264 | // > url.parse('http://google.com:8080')
265 | // Url {
266 | // protocol: 'http:',
267 | // slashes: true,
268 | // auth: null,
269 | // host: 'google.com:8080',
270 | // port: '8080',
271 | // hostname: 'google.com',
272 | // hash: null,
273 | // search: null,
274 | // query: null,
275 | // pathname: '/',
276 | // path: '/',
277 | // href: 'http://google.com:8080/' }
278 | // > url.parse('http://google.com:8080/')
279 | // Url {
280 | // protocol: 'http:',
281 | // slashes: true,
282 | // auth: null,
283 | // host: 'google.com:8080',
284 | // port: '8080',
285 | // hostname: 'google.com',
286 | // hash: null,
287 | // search: null,
288 | // query: null,
289 | // pathname: '/',
290 | // path: '/',
291 | // href: 'http://google.com:8080/' }
292 | result[OperandUrl.uid] = value;
293 | result[OperandHost.uid] = parsedUrl.hostname;
294 | result[OperandScheme.uid] = parsedUrl.protocol;
295 | result[OperandPath.uid] = parsedUrl.path;
296 | result[OperandPort.uid] = parsedUrl.port;
297 |
298 | // TODO
299 | // result[OperandApp.uid] = null;
300 | // result[OperandExtension.uid] = null;
301 |
302 | return result;
303 | }
304 |
305 | function evaluateRule (rule, objectToEvaluate) {
306 | return new RuleEvaluator(rule).evaluate(objectToEvaluate);
307 | }
308 |
309 | function evaluateRules (rules, url, event) {
310 | let objectToEvaluate = crateObjectToEvaluate(url, event);
311 | for (let i = 0; i < rules.length; i++) {
312 | let rule = rules[i];
313 | if (evaluateRule(rule, objectToEvaluate)) {
314 | return rule;
315 | }
316 | }
317 | return null;
318 | }
319 |
320 | module.exports = {
321 | '_OperatorIs': OperatorIs,
322 | '_OperatorIsNot': OperatorIsNot,
323 | '_OperatorContains': OperatorContains,
324 | '_OperatorNotContains': OperatorNotContains,
325 | '_OperatorStartsWith': OperatorStartsWith,
326 | '_OperatorNotStartsWith': OperatorNotStartsWith,
327 | '_OperatorEndsWith': OperatorEndsWith,
328 | '_OperatorNotEndsWith': OperatorNotEndsWith,
329 | '_OperatorRegexp': OperatorRegexp,
330 |
331 | '_OperandApp': OperandApp,
332 | '_OperandHost': OperandHost,
333 | '_OperandScheme': OperandScheme,
334 | '_OperandPath': OperandPath,
335 | '_OperandPort': OperandPort,
336 | '_OperandExtension': OperandExtension,
337 | '_OperandUrL': OperandUrl,
338 |
339 | '_RuleEvaluator': RuleEvaluator,
340 |
341 | 'crateObjectToEvaluate': crateObjectToEvaluate,
342 | 'evaluateRule': evaluateRule,
343 | 'evaluateRules': evaluateRules,
344 | 'getConditionOperators': getConditionOperators,
345 | 'getConditionOperands': getConditionOperands,
346 | 'getConditionOperandsObject': getConditionOperandsObject
347 | };
348 |
--------------------------------------------------------------------------------
/src/app/js/logger.js:
--------------------------------------------------------------------------------
1 | const winston = require('winston');
2 | winston.emitErrs = true;
3 |
4 | function getLogger (logFilename) {
5 | return new winston.Logger({
6 | transports: [
7 | new winston.transports.File({
8 | level: 'info',
9 | filename: logFilename,
10 | handleExceptions: true,
11 | json: false,
12 | maxsize: 5242880, // 5MB
13 | maxFiles: 5,
14 | colorize: false
15 | })
16 | ],
17 | exitOnError: false
18 | });
19 | }
20 |
21 | module.exports = {
22 | 'getLogger': getLogger
23 | };
24 |
--------------------------------------------------------------------------------
/src/app/js/open2.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const exec = require('child_process').exec;
4 |
5 | function escape (s) {
6 | // eslint-disable-next-line no-useless-escape
7 | return s.replace(/"/g, '\\\"');
8 | }
9 |
10 | /**
11 | * open a file or uri using the default application for the file type.
12 | *
13 | * @return {ChildProcess} - the child process object.
14 | * @param {string} target - the file/uri to open.
15 | * @param {string} app - (optional) the application to be used to open the
16 | * file (for example, "chrome", "firefox")
17 | * @param mode
18 | * @param options
19 | * @param args
20 | * an error object that contains a property 'code' with the exit
21 | * code of the process.
22 | */
23 | function open2 (target, app, mode, options = null, args = null) {
24 | app = escape(app);
25 |
26 | let cmd = `open ${mode} "${app}"`;
27 |
28 | if (options) {
29 | cmd = `${cmd} ${options}`;
30 | }
31 |
32 | cmd = `${cmd} "${target}"`;
33 |
34 | if (args) {
35 | cmd = `${cmd} −−args ${args}`;
36 | }
37 |
38 | return exec(cmd);
39 | }
40 |
41 | module.exports = open2;
42 |
--------------------------------------------------------------------------------
/src/app/js/signals.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const GENERAL = {
4 | TEST_URL: 'test-url',
5 | LOAD_URL: 'load-url',
6 | OPEN_URL: 'open-url',
7 | SHOW_MAN_PAGE: 'show-man-page',
8 | ERROR: 'general-error'
9 | };
10 |
11 | const CONFIG = {
12 | READ_STATUS: 'read-status',
13 | UPDATE_STATUS: 'update-status',
14 | READ_IS_USE_DEFAULT: 'read-is-use-default',
15 | UPDATE_IS_USE_DEFAULT: 'update-is-use-default',
16 | TOGGLE_APP_STATUS: 'toggle-app-status',
17 | RESET_ALL: 'reset-all'
18 | };
19 |
20 | const APPLICATION = {
21 | CREATE: 'create-applications',
22 | READ: 'get-applications',
23 | UPDATE: 'update-applications',
24 | DELETE: 'delete-applications',
25 |
26 | CREATE_ONE: 'create-application',
27 | READ_ONE: 'get-application',
28 | UPDATE_ONE: 'update-application',
29 | DELETE_ONE: 'delete-application',
30 |
31 | CLEAR: 'clear-applications',
32 | UPDATE_DEFAULT: 'update-default-application'
33 | };
34 |
35 | const ICON = {
36 | CREATE: 'create-icons',
37 | READ: 'get-icons',
38 | UPDATE: 'update-icons',
39 | DELETE: 'delete-icons',
40 |
41 | CREATE_ONE: 'create-icon',
42 | READ_ONE: 'get-icon',
43 | UPDATE_ONE: 'update-icon',
44 | DELETE_ONE: 'delete-icon',
45 |
46 | CLEAR: 'clear-icons'
47 | };
48 |
49 | const RULE = {
50 | CREATE: 'create-rules',
51 | READ: 'get-rules',
52 | UPDATE: 'update-rules',
53 | DELETE: 'delete-rules',
54 |
55 | CREATE_ONE: 'create-rule',
56 | READ_ONE: 'get-rule',
57 | UPDATE_ONE: 'update-rule',
58 | DELETE_ONE: 'delete-rule',
59 |
60 | CLEAR: 'clear-rules'
61 | };
62 |
63 | function request (signal) {
64 | return `request-${signal}`;
65 | }
66 |
67 | function response (signal) {
68 | return `response-${signal}`;
69 | }
70 |
71 | module.exports = {
72 | 'CONFIG': CONFIG,
73 | 'APPLICATION': APPLICATION,
74 | 'RULE': RULE,
75 | 'ICON': ICON,
76 | 'GENERAL': GENERAL,
77 | 'request': request,
78 | 'response': response
79 | };
80 |
--------------------------------------------------------------------------------
/src/app/js/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const plist = require('plist');
6 | const glob = require('glob');
7 |
8 | function* entries (obj) {
9 | for (let key of Object.keys(obj)) {
10 | yield [key, obj[key]];
11 | }
12 | }
13 |
14 | function objectValues (obj) {
15 | return Object.keys(obj).map(key => obj[key]);
16 | }
17 |
18 | function _isInArray (arr, val) {
19 | return arr.some(arrVal => {
20 | return val === arrVal;
21 | });
22 | }
23 |
24 | function getAppInfo (appPath, onlyBrowsers = true) {
25 | let plistPath = path.join(appPath, 'Contents', 'Info.plist');
26 | let plistParsed = plist.parse(fs.readFileSync(plistPath, 'utf8'));
27 | let urlTypes = plistParsed.CFBundleURLTypes || [];
28 | let wantedSchemes = ['http', 'https', 'ftp', 'file'];
29 | let result = { 'is_browser': false, schemes: [] };
30 |
31 | for (let i = 0; i < urlTypes.length; i++) {
32 | let availableSchemes = urlTypes[i].CFBundleURLSchemes || [];
33 | for (let j = 0; j < wantedSchemes.length; j++) {
34 | let wantedScheme = wantedSchemes[i];
35 | if (_isInArray(availableSchemes, wantedScheme) && result.schemes.indexOf(wantedScheme) === -1) {
36 | result.schemes.push(wantedScheme);
37 | result.is_browser = true;
38 | }
39 | }
40 | }
41 | if (result.is_browser || onlyBrowsers === false) {
42 | result['identifier'] = plistParsed.CFBundleIdentifier;
43 | result['display_name'] = plistParsed.CFBundleDisplayName || plistParsed.CFBundleName;
44 | result['name'] = plistParsed.CFBundleName;
45 | result['executable'] = plistParsed.CFBundleExecutable;
46 | let iconFie = plistParsed.CFBundleIconFile.endsWith('.icns') ? plistParsed.CFBundleIconFile : `${plistParsed.CFBundleIconFile}.icns`;
47 | result['icns'] = path.join(appPath, 'Contents', 'Resources', iconFie);
48 | result['path'] = appPath;
49 | }
50 | return result;
51 | }
52 |
53 | function getApps (appsRoot = '/Applications') {
54 | let result = {errors: [], results: []};
55 | let appsList = glob('*.app', {'cwd': appsRoot, 'sync': true});
56 | for (let i = 0; i < appsList.length; i++) {
57 | let appPath = path.join(appsRoot, appsList[i]);
58 | try {
59 | let appInfo = getAppInfo(appPath);
60 | if (appInfo.is_browser) {
61 | result.results.push(appInfo);
62 | }
63 | } catch (e) {
64 | result.errors.push(e);
65 | }
66 | }
67 | return result;
68 | }
69 |
70 | module.exports = {
71 | 'entries': entries,
72 | 'objectValues': objectValues,
73 | 'getApps': getApps,
74 | 'getAppInfo': getAppInfo
75 | };
76 |
--------------------------------------------------------------------------------
/src/app/templates/edit_application.html:
--------------------------------------------------------------------------------
1 |
86 |
--------------------------------------------------------------------------------
/src/app/templates/edit_condition.html:
--------------------------------------------------------------------------------
1 |
54 |
--------------------------------------------------------------------------------
/src/app/templates/edit_rule.html:
--------------------------------------------------------------------------------
1 |
111 |
--------------------------------------------------------------------------------
/src/app/templates/edit_settings.html:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------
/src/app/templates/list_of_applications.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
17 |
18 |
{{ item.display_name }}
19 |
20 | {{ item.identifier }}
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/app/templates/list_of_rules.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
18 |
19 |
{{ item.name }}
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "src",
3 | "private": true,
4 | "moduleType": [
5 | "es6",
6 | "globals",
7 | "node",
8 | "yui"
9 | ],
10 | "ignore": [
11 | "**/.*",
12 | "node_modules",
13 | "bower_components",
14 | "test",
15 | "tests"
16 | ],
17 | "dependencies": {
18 | "angular": "^1.6.1",
19 | "angular-material": "^1.1.1",
20 | "angular-sanitize": "^1.6.1",
21 | "photon": "^0.1.2-alpha",
22 | "flexboxgrid": "^6.3.2"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/e2e/main.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const Application = require('spectron').Application;
6 | const chai = require('chai');
7 | const chaiAsPromised = require('chai-as-promised');
8 | const electronPath = require('electron');
9 |
10 | const TIMEOUT = process.env.CI ? 30000 : 10000;
11 | const ACTION_TIMEOUT = 1500;
12 | const MAIN_JS = path.resolve(path.join(__dirname, '..', '..', 'main.js'));
13 | const USER_DATA_PATH = path.join(process.env.HOME, 'Library', 'Application Support', 'Electron');
14 | const USER_DATA_PATH_DB = path.join(USER_DATA_PATH, 'databases');
15 | const APPS_ROOT_FAKE = path.resolve(path.join(__dirname, '..', 'test_data', 'Applications'));
16 |
17 | let deleteDatabaseFile = function (fpath) {
18 | if (fs.existsSync(fpath) && fs.statSync(fpath).isFile()) {
19 | fs.unlinkSync(fpath);
20 | }
21 | };
22 |
23 | let clearDatabaseFiles = function () {
24 | for (let fp of [
25 | path.join(USER_DATA_PATH_DB, 'applications.db'),
26 | path.join(USER_DATA_PATH_DB, 'icons.db'),
27 | path.join(USER_DATA_PATH_DB, 'rules.db'),
28 | path.join(USER_DATA_PATH_DB, 'settings.db')
29 | ]) {
30 | deleteDatabaseFile(fp);
31 | }
32 | };
33 |
34 | global.before(function () {
35 | chai.should();
36 | chai.use(chaiAsPromised);
37 | });
38 |
39 | describe('BrowserDispatcher', function () {
40 | this.timeout(TIMEOUT);
41 |
42 | before(function () {
43 | process.env.BROWSER_DISPATCHER_APPS_ROOT = APPS_ROOT_FAKE;
44 | process.env.RUNNING_IN_SPECTRON = true;
45 | clearDatabaseFiles();
46 | });
47 |
48 | after(function () {
49 | delete process.env.BROWSER_DISPATCHER_APPS_ROOT;
50 | delete process.env.RUNNING_IN_SPECTRON;
51 | clearDatabaseFiles();
52 | });
53 |
54 | beforeEach(function () {
55 | this.app = new Application({
56 | path: electronPath,
57 | args: [MAIN_JS]
58 | });
59 | chaiAsPromised.transferPromiseness = this.app.transferPromiseness;
60 | return this.app.start();
61 | });
62 |
63 | afterEach(function () {
64 | deleteDatabaseFile(path.join(USER_DATA_PATH_DB, 'rules.db'));
65 | if (this.app && this.app.isRunning()) {
66 | return this.app.stop();
67 | }
68 | });
69 |
70 | it('starts with main window shown', function (done) {
71 | this.app.client
72 | .waitUntilWindowLoaded()
73 | .windowByIndex(0).browserWindow.isVisible().should.eventually.be.true
74 | .windowByIndex(0).browserWindow.getTitle().should.eventually.equal('Browser Dispatcher')
75 | .notify(done);
76 | });
77 |
78 | it('has no rules predefined', function (done) {
79 | this.app.client
80 | .isVisible('#noRules').should.eventually.be.true
81 | .getText('#noRules').should.eventually.equal('There are no rules configured')
82 | .notify(done);
83 | });
84 |
85 | it('automatically loads list of available applications', function (done) {
86 | this.app.client
87 | .waitUntilWindowLoaded()
88 | .click('#toolbarActionReloadApps')
89 | .waitForVisible('#appsList', ACTION_TIMEOUT)
90 | .isExisting('#noApps').should.eventually.be.false
91 | .getText('#appItemDisplayName_0').should.eventually.be.oneOf(['Safari', 'Google Chrome'])
92 | .getText('#appItemIdentifier_0').should.eventually.be.oneOf(['com.apple.Safari', 'com.google.Chrome'])
93 | .getText('#appItemDisplayName_1').should.eventually.be.oneOf(['Safari', 'Google Chrome'])
94 | .getText('#appItemIdentifier_1').should.eventually.be.oneOf(['com.apple.Safari', 'com.google.Chrome'])
95 | .isExisting('#appDisplayName_2').should.eventually.be.false
96 | .notify(done);
97 | });
98 |
99 | it('adds simple rule', function (done) {
100 | let ruleName = 'My Test Rule';
101 | let conditionText = 'google.com';
102 | this.app.client
103 | .waitUntilWindowLoaded()
104 | .waitForVisible('#noRules', ACTION_TIMEOUT).should.be.fulfilled
105 | .click('#toolbarActionAddNew')
106 | .waitForVisible('#editRule', ACTION_TIMEOUT).should.be.fulfilled
107 | .setValue('#editRuleName', ruleName)
108 | .getValue('#editRuleName').should.eventually.equal(ruleName)
109 | .setValue('#editConditionText_0', conditionText)
110 | .getValue('#editConditionText_0').should.eventually.equal(conditionText)
111 | .click('#toolbarActionSave')
112 | .waitForVisible('#rulesList', ACTION_TIMEOUT).should.be.fulfilled
113 | .getText('#ruleItemName_0').should.eventually.equal(ruleName)
114 | .getText('#ruleItemConditions_0').should.eventually.equal('1 condition')
115 | .getAttribute('#ruleItemAppLogo_0', 'alt').should.eventually.equal('Safari logo').and
116 | .notify(done);
117 | });
118 |
119 | it('configures advanced rule', function (done) {
120 | let ruleName = 'My Test Rule';
121 | this.app.client
122 | .waitUntilWindowLoaded()
123 | .waitForVisible('#noRules', ACTION_TIMEOUT).should.be.fulfilled
124 | .click('#toolbarActionAddNew')
125 | .waitForVisible('#editRule', ACTION_TIMEOUT).should.be.fulfilled
126 | .selectByValue('#editRuleOperator', 'all')
127 | .setValue('#editRuleName', ruleName)
128 | .selectByValue('#editConditionOperand_0', 'host')
129 | .selectByValue('#editConditionOperator_0', 'is')
130 | .setValue('#editConditionText_0', 'github.com')
131 | .click('#editConditionActionAdd_0')
132 | .waitForVisible('#editConditionText_1', ACTION_TIMEOUT).should.be.fulfilled
133 | .selectByValue('#editConditionOperand_1', 'path')
134 | .selectByValue('#editConditionOperator_1', 'starts_with')
135 | .setValue('#editConditionText_1', '/andriyko/')
136 | .selectByVisibleText('#editRuleApplication', 'Google Chrome')
137 | .click('#editRuleAdvancedSettingsOpenNewInstance')
138 | .click('#toolbarActionSave')
139 | .waitForVisible('#rulesList', ACTION_TIMEOUT).should.be.fulfilled
140 | .getText('#ruleItemName_0').should.eventually.equal(ruleName)
141 | .getText('#ruleItemConditions_0').should.eventually.equal('All 2 conditions must match')
142 | .getAttribute('#ruleItemAppLogo_0', 'alt').should.eventually.equal('Google Chrome logo')
143 | .click('#ruleItem_0')
144 | .waitForVisible('#editRule', ACTION_TIMEOUT).should.be.fulfilled
145 | .getValue('#editRuleName').should.eventually.equal(ruleName)
146 | .getValue('#editRuleOperator').should.eventually.equal('all')
147 | .getValue('#editConditionOperand_0').should.eventually.equal('host')
148 | .getValue('#editConditionOperator_0').should.eventually.equal('is')
149 | .getValue('#editConditionText_0').should.eventually.equal('github.com')
150 | .getValue('#editConditionOperand_1').should.eventually.equal('path')
151 | .getValue('#editConditionOperator_1').should.eventually.equal('starts_with')
152 | .getValue('#editConditionText_1').should.eventually.equal('/andriyko/')
153 | .getValue('#editRuleAdvancedSettingsOpenNewInstance').should.eventually.equal('on')
154 | .notify(done);
155 | });
156 |
157 | it('edits rule', function (done) {
158 | let ruleName = 'My Test Rule';
159 | let conditionText = 'google.com';
160 | this.app.client
161 | .waitUntilWindowLoaded()
162 | .waitForVisible('#noRules', ACTION_TIMEOUT).should.be.fulfilled
163 | .click('#toolbarActionAddNew')
164 | .waitForVisible('#editRule', ACTION_TIMEOUT).should.be.fulfilled
165 | .setValue('#editRuleName', ruleName)
166 | .getValue('#editRuleName').should.eventually.equal(ruleName)
167 | .setValue('#editConditionText_0', conditionText)
168 | .getValue('#editConditionText_0').should.eventually.equal(conditionText)
169 | .click('#toolbarActionSave')
170 | .waitForVisible('#rulesList', ACTION_TIMEOUT).should.be.fulfilled
171 | .click('#ruleItem_0')
172 | .waitForVisible('#editRule', ACTION_TIMEOUT).should.be.fulfilled
173 | .setValue('#editRuleName', `${ruleName} Edited`)
174 | .getValue('#editRuleName').should.eventually.equal(`${ruleName} Edited`)
175 | .setValue('#editConditionText_0', `${conditionText}.ua`)
176 | .getValue('#editConditionText_0').should.eventually.equal(`${conditionText}.ua`)
177 | .click('#editConditionActionAdd_0')
178 | .setValue('#editConditionText_1', conditionText)
179 | .getValue('#editConditionText_1').should.eventually.equal(conditionText)
180 | .click('#toolbarActionSave')
181 | .waitForVisible('#rulesList', ACTION_TIMEOUT).should.be.fulfilled
182 | .getText('#ruleItemName_0').should.eventually.equal(`${ruleName} Edited`)
183 | .getText('#ruleItemConditions_0').should.eventually.equal('Any 2 conditions must match').and
184 | .notify(done);
185 | });
186 |
187 | it('deletes rule', function (done) {
188 | let ruleName = 'My Test Rule';
189 | let conditionText = 'google.com';
190 | this.app.client
191 | .waitUntilWindowLoaded()
192 | .waitForVisible('#noRules', ACTION_TIMEOUT).should.be.fulfilled
193 | .click('#toolbarActionAddNew')
194 | .waitForVisible('#editRule', ACTION_TIMEOUT).should.be.fulfilled
195 | .setValue('#editRuleName', ruleName)
196 | .setValue('#editConditionText_0', conditionText)
197 | .click('#toolbarActionSave')
198 | .waitForVisible('#rulesList', ACTION_TIMEOUT).should.be.fulfilled
199 | .click('#ruleItem_0')
200 | .click('#toolbarActionDelete')
201 | .waitForVisible('#noRules', ACTION_TIMEOUT).should.be.fulfilled
202 | .notify(done);
203 | });
204 |
205 | it('enables/disables the application', function (done) {
206 | this.app.client
207 | .waitUntilWindowLoaded()
208 | .click('#toolbarActionReloadSettings')
209 | .waitForVisible('#editSettings', ACTION_TIMEOUT).should.be.fulfilled
210 | .isSelected('#editSettingsAppStatus').should.eventually.be.true
211 | .isSelected('#editSettingsAppUseDefault').should.eventually.be.false
212 | .click('#editSettingsAppStatus')
213 | .click('#toolbarActionSave')
214 | .click('#toolbarActionReloadRules')
215 | .waitForVisible('#noRules', ACTION_TIMEOUT).should.be.fulfilled
216 | .click('#toolbarActionReloadSettings')
217 | .isSelected('#editSettingsAppStatus').should.eventually.be.false
218 | .notify(done);
219 | });
220 | });
221 |
--------------------------------------------------------------------------------
/tests/test_data/Applications/Calculator.app/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildMachineOSBuild
6 | 16C30
7 | CFBundleDevelopmentRegion
8 | English
9 | CFBundleExecutable
10 | Calculator
11 | CFBundleGetInfoString
12 | 10.8, Copyright © 2001-2013, Apple Inc.
13 | CFBundleHelpBookFolder
14 | Calculator.help
15 | CFBundleHelpBookName
16 | com.apple.Calculator.help
17 | CFBundleIconFile
18 | AppIcon
19 | CFBundleIdentifier
20 | com.apple.calculator
21 | CFBundleInfoDictionaryVersion
22 | 6.0
23 | CFBundleName
24 | Calculator
25 | CFBundlePackageType
26 | APPL
27 | CFBundleShortVersionString
28 | 10.8
29 | CFBundleSignature
30 | ????
31 | CFBundleSupportedPlatforms
32 |
33 | MacOSX
34 |
35 | CFBundleVersion
36 | 123
37 | DTCompiler
38 | com.apple.compilers.llvm.clang.1_0
39 | DTPlatformBuild
40 | 8R174l
41 | DTPlatformVersion
42 | GM
43 | DTSDKBuild
44 | 16C30
45 | DTSDKName
46 | macosx10.12internal
47 | DTXcode
48 | 0800
49 | DTXcodeBuild
50 | 8R174l
51 | LSApplicationCategoryType
52 | public.app-category.utilities
53 | LSApplicationSecondaryCategoryType
54 | public.app-category.productivity
55 | LSHasLocalizedDisplayName
56 |
57 | LSMinimumSystemVersion
58 | 10.10.0
59 | NSMainNibFile
60 | Calculator
61 | NSPrincipalClass
62 | NSApplication
63 | NSSupportsSuddenTermination
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/tests/test_data/Applications/Google Chrome.app/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildMachineOSBuild
6 | 13F1077
7 | CFBundleDevelopmentRegion
8 | en
9 | CFBundleDisplayName
10 | Google Chrome
11 | CFBundleDocumentTypes
12 |
13 |
14 | CFBundleTypeExtensions
15 |
16 | gif
17 |
18 | CFBundleTypeIconFile
19 | document.icns
20 | CFBundleTypeMIMETypes
21 |
22 | image/gif
23 |
24 | CFBundleTypeName
25 | GIF image
26 | CFBundleTypeOSTypes
27 |
28 | GIFf
29 |
30 | CFBundleTypeRole
31 | Viewer
32 |
33 |
34 | CFBundleTypeExtensions
35 |
36 | html
37 | htm
38 |
39 | CFBundleTypeIconFile
40 | document.icns
41 | CFBundleTypeMIMETypes
42 |
43 | text/html
44 |
45 | CFBundleTypeName
46 | HTML document
47 | CFBundleTypeOSTypes
48 |
49 | HTML
50 |
51 | CFBundleTypeRole
52 | Viewer
53 |
54 |
55 | CFBundleTypeExtensions
56 |
57 | xhtml
58 |
59 | CFBundleTypeIconFile
60 | document.icns
61 | CFBundleTypeMIMETypes
62 |
63 | text/xhtml
64 |
65 | CFBundleTypeName
66 | XHTML document
67 | CFBundleTypeRole
68 | Viewer
69 |
70 |
71 | CFBundleTypeExtensions
72 |
73 | js
74 |
75 | CFBundleTypeIconFile
76 | document.icns
77 | CFBundleTypeMIMETypes
78 |
79 | application/x-javascript
80 |
81 | CFBundleTypeName
82 | JavaScript script
83 | CFBundleTypeRole
84 | Viewer
85 |
86 |
87 | CFBundleTypeExtensions
88 |
89 | jpg
90 | jpeg
91 |
92 | CFBundleTypeIconFile
93 | document.icns
94 | CFBundleTypeMIMETypes
95 |
96 | image/jpeg
97 |
98 | CFBundleTypeName
99 | JPEG image
100 | CFBundleTypeOSTypes
101 |
102 | JPEG
103 |
104 | CFBundleTypeRole
105 | Viewer
106 |
107 |
108 | CFBundleTypeExtensions
109 |
110 | mhtml
111 | mht
112 |
113 | CFBundleTypeIconFile
114 | document.icns
115 | CFBundleTypeMIMETypes
116 |
117 | multipart/related
118 | application/x-mimearchive
119 | message/rfc822
120 |
121 | CFBundleTypeName
122 | MHTML document
123 | CFBundleTypeRole
124 | Viewer
125 |
126 |
127 | CFBundleTypeExtensions
128 |
129 | oga
130 | ogg
131 |
132 | CFBundleTypeIconFile
133 | document.icns
134 | CFBundleTypeMIMETypes
135 |
136 | audio/ogg
137 |
138 | CFBundleTypeName
139 | HTML5 Audio (Ogg)
140 | CFBundleTypeRole
141 | Viewer
142 |
143 |
144 | CFBundleTypeExtensions
145 |
146 | ogv
147 |
148 | CFBundleTypeIconFile
149 | document.icns
150 | CFBundleTypeMIMETypes
151 |
152 | video/ogg
153 |
154 | CFBundleTypeName
155 | HTML5 Video (Ogg)
156 | CFBundleTypeRole
157 | Viewer
158 |
159 |
160 | CFBundleTypeExtensions
161 |
162 | png
163 |
164 | CFBundleTypeIconFile
165 | document.icns
166 | CFBundleTypeMIMETypes
167 |
168 | image/png
169 |
170 | CFBundleTypeName
171 | PNG image
172 | CFBundleTypeOSTypes
173 |
174 | PNGf
175 |
176 | CFBundleTypeRole
177 | Viewer
178 |
179 |
180 | CFBundleTypeExtensions
181 |
182 | svg
183 |
184 | CFBundleTypeIconFile
185 | document.icns
186 | CFBundleTypeMIMETypes
187 |
188 | image/svg+xml
189 |
190 | CFBundleTypeName
191 | SVG document
192 | CFBundleTypeRole
193 | Viewer
194 |
195 |
196 | CFBundleTypeExtensions
197 |
198 | txt
199 | text
200 |
201 | CFBundleTypeIconFile
202 | document.icns
203 | CFBundleTypeMIMETypes
204 |
205 | text/plain
206 |
207 | CFBundleTypeName
208 | Plain text document
209 | CFBundleTypeOSTypes
210 |
211 | TEXT
212 |
213 | CFBundleTypeRole
214 | Viewer
215 |
216 |
217 | CFBundleTypeExtensions
218 |
219 | webm
220 |
221 | CFBundleTypeIconFile
222 | document.icns
223 | CFBundleTypeMIMETypes
224 |
225 | video/webm
226 |
227 | CFBundleTypeName
228 | HTML5 Video (WebM)
229 | CFBundleTypeRole
230 | Viewer
231 |
232 |
233 | CFBundleTypeExtensions
234 |
235 | webp
236 |
237 | CFBundleTypeIconFile
238 | document.icns
239 | CFBundleTypeMIMETypes
240 |
241 | image/webp
242 |
243 | CFBundleTypeName
244 | WebP image
245 | CFBundleTypeRole
246 | Viewer
247 |
248 |
249 | CFBundleTypeRole
250 | Viewer
251 | LSItemContentTypes
252 |
253 | org.chromium.extension
254 |
255 |
256 |
257 | CFBundleTypeExtensions
258 |
259 | pdf
260 |
261 | CFBundleTypeIconFile
262 | document.icns
263 | CFBundleTypeMIMETypes
264 |
265 | application/pdf
266 |
267 | CFBundleTypeName
268 | PDF Document
269 | CFBundleTypeRole
270 | Viewer
271 |
272 |
273 | CFBundleExecutable
274 | Google Chrome
275 | CFBundleIconFile
276 | app.icns
277 | CFBundleIdentifier
278 | com.google.Chrome
279 | CFBundleInfoDictionaryVersion
280 | 6.0
281 | CFBundleName
282 | Chrome
283 | CFBundlePackageType
284 | APPL
285 | CFBundleShortVersionString
286 | 55.0.2883.95
287 | CFBundleSignature
288 | rimZ
289 | CFBundleURLTypes
290 |
291 |
292 | CFBundleURLName
293 | Web site URL
294 | CFBundleURLSchemes
295 |
296 | http
297 | https
298 |
299 |
300 |
301 | CFBundleURLName
302 | FTP site URL
303 | CFBundleURLSchemes
304 |
305 | ftp
306 |
307 |
308 |
309 | CFBundleURLName
310 | Local file URL
311 | CFBundleURLSchemes
312 |
313 | file
314 |
315 |
316 |
317 | CFBundleVersion
318 | 2883.95
319 | DTCompiler
320 | com.apple.compilers.llvm.clang.1_0
321 | DTSDKBuild
322 | 10.10
323 | DTSDKName
324 | macosx10.10
325 | DTXcode
326 | 0511
327 | DTXcodeBuild
328 | 5B1008
329 | KSChannelID-full
330 | -full
331 | KSProductID
332 | com.google.Chrome
333 | KSUpdateURL
334 | https://tools.google.com/service/update2
335 | KSVersion
336 | 55.0.2883.95
337 | LSFileQuarantineEnabled
338 |
339 | LSHasLocalizedDisplayName
340 | 1
341 | LSMinimumSystemVersion
342 | 10.9.0
343 | NSAppleScriptEnabled
344 |
345 | NSPrincipalClass
346 | BrowserCrApplication
347 | NSSupportsAutomaticGraphicsSwitching
348 |
349 | NSUserActivityTypes
350 |
351 | NSUserActivityTypeBrowsingWeb
352 |
353 | NSUserNotificationAlertStyle
354 | banner
355 | OSAScriptingDefinition
356 | scripting.sdef
357 | SCMRevision
358 | a97b1aab33877677c4734c58ed41246aa40942b7-refs/branch-heads/2883@{#738}
359 | UTExportedTypeDeclarations
360 |
361 |
362 | UTTypeConformsTo
363 |
364 | public.data
365 |
366 | UTTypeDescription
367 | Chromium Extra
368 | UTTypeIdentifier
369 | org.chromium.extension
370 | UTTypeTagSpecification
371 |
372 | public.filename-extension
373 |
374 | crx
375 |
376 |
377 |
378 |
379 |
380 |
381 |
--------------------------------------------------------------------------------
/tests/test_data/Applications/Google Chrome.app/Contents/Resources/app.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/tests/test_data/Applications/Google Chrome.app/Contents/Resources/app.icns
--------------------------------------------------------------------------------
/tests/test_data/Applications/Safari.app/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Application-Group
6 |
7 | dot-mac
8 | InternetAccounts
9 |
10 | BuildMachineOSBuild
11 | 16C56
12 | CFBundleDevelopmentRegion
13 | English
14 | CFBundleDocumentTypes
15 |
16 |
17 | CFBundleTypeExtensions
18 |
19 | css
20 |
21 | CFBundleTypeIconFile
22 | css.icns
23 | CFBundleTypeMIMETypes
24 |
25 | text/css
26 |
27 | CFBundleTypeName
28 | CSS style sheet
29 | CFBundleTypeRole
30 | Viewer
31 | LSHandlerRank
32 | Default
33 | NSDocumentClass
34 | BrowserDocument
35 |
36 |
37 | CFBundleTypeExtensions
38 |
39 | pdf
40 |
41 | CFBundleTypeIconFile
42 | pdf.icns
43 | CFBundleTypeMIMETypes
44 |
45 | application/pdf
46 |
47 | CFBundleTypeName
48 | PDF document
49 | CFBundleTypeRole
50 | Viewer
51 | LSHandlerRank
52 | Default
53 | NSDocumentClass
54 | BrowserDocument
55 |
56 |
57 | CFBundleTypeExtensions
58 |
59 | webarchive
60 |
61 | CFBundleTypeIconFile
62 | webarchive.icns
63 | CFBundleTypeMIMETypes
64 |
65 | application/x-webarchive
66 |
67 | CFBundleTypeName
68 | Web archive
69 | CFBundleTypeRole
70 | Viewer
71 | ICExtension
72 | ARCHIVE
73 | LSHandlerRank
74 | Default
75 | LSIsAppleDefaultForType
76 |
77 | NSDocumentClass
78 | BrowserDocument
79 |
80 |
81 | CFBundleTypeExtensions
82 |
83 | webbookmark
84 |
85 | CFBundleTypeIconFile
86 | webbookmark.icns
87 | CFBundleTypeName
88 | Safari bookmark
89 | CFBundleTypeRole
90 | Viewer
91 | LSHandlerRank
92 | Default
93 | NSDocumentClass
94 | BrowserDocument
95 |
96 |
97 | CFBundleTypeExtensions
98 |
99 | webhistory
100 |
101 | CFBundleTypeIconFile
102 | webhistory.icns
103 | CFBundleTypeName
104 | Safari history item
105 | CFBundleTypeRole
106 | Viewer
107 | LSHandlerRank
108 | Default
109 | NSDocumentClass
110 | BrowserDocument
111 |
112 |
113 | CFBundleTypeExtensions
114 |
115 | webloc
116 |
117 | CFBundleTypeIconFile
118 | webloc.icns
119 | CFBundleTypeName
120 | Web internet location
121 | CFBundleTypeOSTypes
122 |
123 | ilht
124 |
125 | CFBundleTypeRole
126 | Viewer
127 | LSHandlerRank
128 | Default
129 | NSDocumentClass
130 | BrowserDocument
131 |
132 |
133 | CFBundleTypeExtensions
134 |
135 | download
136 |
137 | CFBundleTypeIconFile
138 | download10.icns
139 | CFBundleTypeName
140 | Safari download
141 | CFBundleTypeRole
142 | Editor
143 | LSHandlerRank
144 | Default
145 | LSTypeIsPackage
146 |
147 | NSDocumentClass
148 | BrowserDocument
149 |
150 |
151 | CFBundleTypeExtensions
152 |
153 | safariextz
154 |
155 | CFBundleTypeIconFile
156 | safariextz.icns
157 | CFBundleTypeMIMETypes
158 |
159 | application/x-safari-extension
160 |
161 | CFBundleTypeName
162 | Safari extension
163 | CFBundleTypeRole
164 | Viewer
165 | LSHandlerRank
166 | Owner
167 | LSTypeIsPackage
168 |
169 | NSDocumentClass
170 | BrowserDocument
171 |
172 |
173 | CFBundleTypeExtensions
174 |
175 | gif
176 |
177 | CFBundleTypeIconFile
178 | gif.icns
179 | CFBundleTypeMIMETypes
180 |
181 | image/gif
182 |
183 | CFBundleTypeName
184 | GIF image
185 | CFBundleTypeOSTypes
186 |
187 | GIFf
188 |
189 | CFBundleTypeRole
190 | Viewer
191 | LSHandlerRank
192 | Default
193 | NSDocumentClass
194 | BrowserDocument
195 |
196 |
197 | CFBundleTypeExtensions
198 |
199 | html
200 | htm
201 | shtml
202 | jhtml
203 |
204 | CFBundleTypeIconFile
205 | html.icns
206 | CFBundleTypeMIMETypes
207 |
208 | text/html
209 |
210 | CFBundleTypeName
211 | HTML document
212 | CFBundleTypeOSTypes
213 |
214 | HTML
215 |
216 | CFBundleTypeRole
217 | Viewer
218 | ICExtension
219 | HTML
220 | LSHandlerRank
221 | Default
222 | LSIsAppleDefaultForType
223 |
224 | NSDocumentClass
225 | BrowserDocument
226 |
227 |
228 | CFBundleTypeExtensions
229 |
230 | js
231 |
232 | CFBundleTypeIconFile
233 | js.icns
234 | CFBundleTypeMIMETypes
235 |
236 | application/x-javascript
237 |
238 | CFBundleTypeName
239 | JavaScript script
240 | CFBundleTypeRole
241 | Viewer
242 | LSHandlerRank
243 | Default
244 | NSDocumentClass
245 | BrowserDocument
246 |
247 |
248 | CFBundleTypeExtensions
249 |
250 | jpg
251 | jpeg
252 |
253 | CFBundleTypeIconFile
254 | jpeg.icns
255 | CFBundleTypeMIMETypes
256 |
257 | image/jpeg
258 |
259 | CFBundleTypeName
260 | JPEG image
261 | CFBundleTypeOSTypes
262 |
263 | JPEG
264 |
265 | CFBundleTypeRole
266 | Viewer
267 | ICExtension
268 | JPEG
269 | LSHandlerRank
270 | Default
271 | NSDocumentClass
272 | BrowserDocument
273 |
274 |
275 | CFBundleTypeExtensions
276 |
277 | jp2
278 |
279 | CFBundleTypeIconFile
280 | jp2.icns
281 | CFBundleTypeMIMETypes
282 |
283 | image/jp2
284 |
285 | CFBundleTypeName
286 | JPEG 2000 image
287 | CFBundleTypeOSTypes
288 |
289 | jp2
290 |
291 | CFBundleTypeRole
292 | Viewer
293 | LSHandlerRank
294 | Default
295 | NSDocumentClass
296 | BrowserDocument
297 |
298 |
299 | CFBundleTypeExtensions
300 |
301 | txt
302 | text
303 |
304 | CFBundleTypeIconFile
305 | txt.icns
306 | CFBundleTypeMIMETypes
307 |
308 | text/plain
309 |
310 | CFBundleTypeName
311 | Plain text document
312 | CFBundleTypeOSTypes
313 |
314 | TEXT
315 |
316 | CFBundleTypeRole
317 | Viewer
318 | ICExtension
319 | TXT
320 | LSHandlerRank
321 | Default
322 | NSDocumentClass
323 | BrowserDocument
324 |
325 |
326 | CFBundleTypeExtensions
327 |
328 | png
329 |
330 | CFBundleTypeIconFile
331 | png.icns
332 | CFBundleTypeMIMETypes
333 |
334 | image/png
335 |
336 | CFBundleTypeName
337 | PNG image
338 | CFBundleTypeOSTypes
339 |
340 | PNGf
341 |
342 | CFBundleTypeRole
343 | Viewer
344 | LSHandlerRank
345 | Default
346 | NSDocumentClass
347 | BrowserDocument
348 |
349 |
350 | CFBundleTypeExtensions
351 |
352 | tiff
353 | tif
354 |
355 | CFBundleTypeIconFile
356 | tiff.icns
357 | CFBundleTypeMIMETypes
358 |
359 | image/tiff
360 |
361 | CFBundleTypeName
362 | TIFF image
363 | CFBundleTypeOSTypes
364 |
365 | TIFF
366 |
367 | CFBundleTypeRole
368 | Viewer
369 | ICExtension
370 | TIFF
371 | LSHandlerRank
372 | Default
373 | NSDocumentClass
374 | BrowserDocument
375 |
376 |
377 | CFBundleTypeExtensions
378 |
379 | url
380 |
381 | CFBundleTypeIconFile
382 | url.icns
383 | CFBundleTypeName
384 | Web site location
385 | CFBundleTypeOSTypes
386 |
387 | LINK
388 |
389 | CFBundleTypeRole
390 | Viewer
391 | LSHandlerRank
392 | Default
393 | LSIsAppleDefaultForType
394 |
395 | NSDocumentClass
396 | BrowserDocument
397 |
398 |
399 | CFBundleTypeExtensions
400 |
401 | ico
402 |
403 | CFBundleTypeIconFile
404 | ico.icns
405 | CFBundleTypeMIMETypes
406 |
407 | image/x-icon
408 |
409 | CFBundleTypeName
410 | Windows icon image
411 | CFBundleTypeOSTypes
412 |
413 | ICO
414 |
415 | CFBundleTypeRole
416 | Viewer
417 | LSHandlerRank
418 | Default
419 | NSDocumentClass
420 | BrowserDocument
421 |
422 |
423 | CFBundleTypeExtensions
424 |
425 | xhtml
426 | xht
427 | xhtm
428 | xht
429 |
430 | CFBundleTypeIconFile
431 | xhtml.icns
432 | CFBundleTypeMIMETypes
433 |
434 | application/xhtml+xml
435 |
436 | CFBundleTypeName
437 | XHTML document
438 | CFBundleTypeRole
439 | Viewer
440 | ICExtension
441 | XHTML
442 | LSHandlerRank
443 | Default
444 | NSDocumentClass
445 | BrowserDocument
446 |
447 |
448 | CFBundleTypeExtensions
449 |
450 | xml
451 | xbl
452 | xsl
453 | xslt
454 |
455 | CFBundleTypeIconFile
456 | xml.icns
457 | CFBundleTypeMIMETypes
458 |
459 | application/xml
460 | text/xml
461 |
462 | CFBundleTypeName
463 | XML document
464 | CFBundleTypeRole
465 | Viewer
466 | ICExtension
467 | XML
468 | LSHandlerRank
469 | Default
470 | NSDocumentClass
471 | BrowserDocument
472 |
473 |
474 | CFBundleTypeExtensions
475 |
476 | svg
477 |
478 | CFBundleTypeIconFile
479 | svg.icns
480 | CFBundleTypeMIMETypes
481 |
482 | image/svg+xml
483 |
484 | CFBundleTypeName
485 | SVG document
486 | CFBundleTypeRole
487 | Viewer
488 | LSHandlerRank
489 | Default
490 | NSDocumentClass
491 | BrowserDocument
492 |
493 |
494 | CFBundleExecutable
495 | Safari
496 | CFBundleGetInfoString
497 | 10.0.2, Copyright © 2003-2016 Apple Inc.
498 | CFBundleHelpBookFolder
499 | Safari.help
500 | CFBundleHelpBookName
501 | com.apple.Safari.help
502 | CFBundleIconFile
503 | compass
504 | CFBundleIdentifier
505 | com.apple.Safari
506 | CFBundleInfoDictionaryVersion
507 | 6.0
508 | CFBundleName
509 | Safari
510 | CFBundlePackageType
511 | APPL
512 | CFBundleShortVersionString
513 | 10.0.2
514 | CFBundleSignature
515 | sfri
516 | CFBundleSupportedPlatforms
517 |
518 | MacOSX
519 |
520 | CFBundleURLTypes
521 |
522 |
523 | CFBundleURLName
524 | Web site URL
525 | CFBundleURLSchemes
526 |
527 | http
528 | https
529 |
530 | LSHandlerRank
531 | Default
532 | LSIsAppleDefaultForScheme
533 |
534 |
535 |
536 | CFBundleURLName
537 | Local file URL
538 | CFBundleURLSchemes
539 |
540 | file
541 |
542 | LSHandlerRank
543 | Default
544 |
545 |
546 | CFBundleVersion
547 | 12602.3.12.0.1
548 | DTCompiler
549 | com.apple.compilers.llvm.clang.1_0
550 | DTPlatformBuild
551 | 8R174l
552 | DTPlatformVersion
553 | GM
554 | DTSDKBuild
555 | 16C56
556 | DTSDKName
557 | macosx10.12internal
558 | DTXcode
559 | 0800
560 | DTXcodeBuild
561 | 8R174l
562 | LSApplicationCategoryType
563 | public.app-category.productivity
564 | LSFileQuarantineEnabled
565 |
566 | LSMinimumSystemVersion
567 | 10.12.0
568 | NSAppleScriptEnabled
569 | Yes
570 | NSLocationUsageDescription
571 | Websites you visit may request your location.
572 | NSMainNibFile
573 | MainMenu
574 | NSPrincipalClass
575 | BrowserApplication
576 | NSServices
577 |
578 |
579 | NSKeyEquivalent
580 |
581 | default
582 | L
583 |
584 | NSMenuItem
585 |
586 | default
587 | Search With %WebSearchProvider@
588 |
589 | NSMessage
590 | searchWithWebSearchProvider
591 | NSPortName
592 | Safari
593 | NSSendTypes
594 |
595 | public.utf8-plain-text
596 |
597 |
598 |
599 | NSMenuItem
600 |
601 | default
602 | Add to Reading List
603 |
604 | NSMessage
605 | addToReadingList
606 | NSPortName
607 | Safari
608 | NSRequiredContext
609 |
610 |
611 | NSTextContent
612 | URL
613 |
614 |
615 | NSLinkSchemes
616 |
617 | http
618 | https
619 |
620 |
621 |
622 | NSSendTypes
623 |
624 | public.rtf
625 | public.utf8-plain-text
626 |
627 |
628 |
629 | NSSupportsAutomaticTermination
630 |
631 | NSSupportsSuddenTermination
632 |
633 | NSUserActivityTypes
634 |
635 | NSUserActivityTypeBrowsingWeb
636 |
637 | UTExportedTypeDeclarations
638 |
639 |
640 | UTTypeConformsTo
641 |
642 | public.data
643 |
644 | UTTypeDescription
645 | Safari bookmark
646 | UTTypeIdentifier
647 | com.apple.safari.bookmark
648 | UTTypeTagSpecification
649 |
650 | public.filename-extension
651 |
652 | webbookmark
653 |
654 |
655 |
656 |
657 | UTTypeConformsTo
658 |
659 | public.data
660 |
661 | UTTypeDescription
662 | Safari extension
663 | UTTypeIdentifier
664 | com.apple.safari.extension
665 | UTTypeTagSpecification
666 |
667 | public.filename-extension
668 |
669 | safariextz
670 |
671 |
672 |
673 |
674 | UTTypeConformsTo
675 |
676 | public.data
677 |
678 | UTTypeDescription
679 | Safari history item
680 | UTTypeIdentifier
681 | com.apple.safari.history
682 | UTTypeTagSpecification
683 |
684 | public.filename-extension
685 |
686 | webhistory
687 |
688 |
689 |
690 |
691 |
692 |
693 |
--------------------------------------------------------------------------------
/tests/test_data/Applications/Safari.app/Contents/Resources/compass.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andriyko/browser-dispatcher/1356b1a60c31e7e4993ac461fb3f0c1db951e6b5/tests/test_data/Applications/Safari.app/Contents/Resources/compass.icns
--------------------------------------------------------------------------------
/tests/unit/evaluators.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const chai = require('chai');
4 | const expect = chai.expect; // we are using the "expect" style of Chai
5 | const evaluators = require('../.././src/app/js/evaluators');
6 |
7 | let TEST_STRING = 'test';
8 |
9 | let RULE_SIMPLE_ANY_CONDITION = {
10 | _id: 'c0GunX7bGVp6azrw',
11 | name: 'Integrations',
12 | is_active: true,
13 | operator: 'any',
14 | browser: {
15 | _id: 'Cat1TxlqAgair0BQ',
16 | name: 'Safari',
17 | path: '/Applications/Safari.app',
18 | icns: '/Applications/Safari.app/Contents/Resources/compass.icns',
19 | display_name: 'Safari',
20 | executable: 'Safari',
21 | identifier: 'com.apple.Safari',
22 | is_default: true,
23 | is_active: true
24 | },
25 | open_new_instance: false,
26 | open_not_foreground: false,
27 | open_fresh: false,
28 | use_app_executable: false,
29 | open_args: '',
30 | conditions: [
31 | {
32 | text: 'salesforce.com',
33 | operand: 'host',
34 | operator: 'contains'
35 | },
36 | {
37 | text: 'developerforce.com',
38 | operand: 'host',
39 | operator: 'contains'
40 | },
41 | {
42 | text: 'service-now.com',
43 | operand: 'host',
44 | operator: 'contains'
45 | },
46 | {
47 | text: 'my.okta.com',
48 | operand: 'host',
49 | operator: 'is'
50 | },
51 | {
52 | text: 'console.aws.amazon.com',
53 | operand: 'host',
54 | operator: 'is'
55 | }
56 | ]
57 | };
58 |
59 | let RULE_SIMPLE_ALL_CONDITIONS = {
60 | _id: 'cxrzMcPAACGvGXtH',
61 | name: 'andriyko Github',
62 | is_active: true,
63 | operator: 'all',
64 | browser: {
65 | _id: 'REIbACm6FRbU1SHH',
66 | name: 'Opera',
67 | path: '/Applications/Opera.app',
68 | icns: '/Applications/Opera.app/Contents/Resources/app.icns',
69 | display_name: 'Opera',
70 | executable: 'Opera',
71 | identifier: 'com.operasoftware.Opera',
72 | is_default: false,
73 | is_active: true
74 | },
75 | open_new_instance: false,
76 | open_not_foreground: false,
77 | open_fresh: false,
78 | use_app_executable: false,
79 | open_args: '',
80 | conditions: [
81 | {
82 | text: 'github.com',
83 | operand: 'host',
84 | operator: 'is' },
85 | {
86 | text: '/andriyko',
87 | operand: 'path',
88 | operator: 'starts_with'
89 | }
90 | ]
91 | };
92 |
93 | let RULE_ADVANCED_OPTIONS = {
94 | _id: 'S4H1HASJ8FB41WwQ',
95 | name: 'Social',
96 | is_active: true,
97 | operator: 'any',
98 | browser: {
99 | _id: 'AcWgWPZbo7SPP5lY',
100 | name: 'Firefox',
101 | path: '/Applications/Firefox.app',
102 | icns: '/Applications/Firefox.app/Contents/Resources/firefox.icns',
103 | display_name: 'Firefox',
104 | executable: 'firefox',
105 | identifier: 'org.mozilla.firefox',
106 | is_default: false,
107 | is_active: true
108 | },
109 | open_new_instance: true,
110 | open_not_foreground: false,
111 | open_fresh: false,
112 | use_app_executable: false,
113 | open_args: '-P “SocialNetworking”',
114 | conditions: [
115 | {
116 | text: 'facebook.com',
117 | operand: 'host',
118 | operator: 'is'
119 | },
120 | {
121 | text: 'instagram.com',
122 | operand: 'host',
123 | operator: 'is'
124 | },
125 | {
126 | text: 'linkedin.com',
127 | operand: 'host',
128 | operator: 'is'
129 | },
130 | {
131 | text: 'reddit.com',
132 | operand: 'host',
133 | operator: 'is'
134 | }
135 | ]
136 | };
137 |
138 | let RULE_INACTIVE = {
139 | _id: 'oeJGcxq5zvGAuskW',
140 | name: 'Local Dev',
141 | is_active: false,
142 | operator: 'any',
143 | browser: {
144 | _id: 'XlJxf2AIAGFdtZ2J',
145 | name: 'Chrome',
146 | path: '/Applications/Google Chrome.app',
147 | icns: '/Applications/Google Chrome.app/Contents/Resources/app.icns',
148 | display_name: 'Google Chrome',
149 | executable: 'Google Chrome',
150 | identifier: 'com.google.Chrome',
151 | is_default: false,
152 | is_active: true
153 | },
154 | open_new_instance: false,
155 | open_not_foreground: false,
156 | open_fresh: false,
157 | use_app_executable: false,
158 | open_args: '',
159 | conditions: [
160 | {
161 | text: '15672',
162 | operand: 'port',
163 | operator: 'is'
164 | },
165 | {
166 | text: '5555',
167 | operand: 'port',
168 | operator: 'is'
169 | },
170 | {
171 | text: '5601',
172 | operand: 'port',
173 | operator: 'is'
174 | },
175 | {
176 | text: '6555',
177 | operand: 'port',
178 | operator: 'is'
179 | },
180 | {
181 | text: '6543',
182 | operand: 'port',
183 | operator: 'is'
184 | },
185 | {
186 | text: '6545',
187 | operand: 'port',
188 | operator: 'is'
189 | }
190 | ]
191 | };
192 |
193 | let RULES = [
194 | RULE_SIMPLE_ANY_CONDITION,
195 | RULE_SIMPLE_ALL_CONDITIONS,
196 | RULE_ADVANCED_OPTIONS,
197 | RULE_INACTIVE
198 | ];
199 |
200 | describe('Operands', function () {
201 | describe('OperandApp', function () {
202 | it('supportedOperators() of OperandApp returns correct list of operators', function () {
203 | expect(evaluators._OperandApp.uid).to.be.equal('app');
204 | expect(evaluators._OperandApp.supportedOperators).to.deep.equal(['is', 'is_not']);
205 | });
206 | });
207 |
208 | describe('OperandHost', function () {
209 | it('supportedOperators() of OperandHost returns correct list of operators', function () {
210 | expect(evaluators._OperandHost.uid).to.be.equal('host');
211 | expect(evaluators._OperandHost.supportedOperators).to.deep.equal([
212 | 'is',
213 | 'is_not',
214 | 'contains',
215 | 'not_contains',
216 | 'starts_with',
217 | 'not_starts_with',
218 | 'ends_with',
219 | 'not_ends_with']);
220 | });
221 | });
222 |
223 | describe('OperandScheme', function () {
224 | it('supportedOperators() of OperandScheme returns correct list of operators', function () {
225 | expect(evaluators._OperandScheme.uid).to.be.equal('scheme');
226 | expect(evaluators._OperandScheme.supportedOperators).to.deep.equal(['is', 'is_not']);
227 | });
228 | });
229 |
230 | describe('OperandPath', function () {
231 | it('supportedOperators() of OperandPath returns correct list of operators', function () {
232 | expect(evaluators._OperandPath.uid).to.be.equal('path');
233 | expect(evaluators._OperandPath.supportedOperators).to.deep.equal([
234 | 'is',
235 | 'is_not',
236 | 'contains',
237 | 'not_contains',
238 | 'starts_with',
239 | 'not_starts_with',
240 | 'ends_with',
241 | 'not_ends_with']);
242 | });
243 | });
244 |
245 | describe('OperandPort', function () {
246 | it('supportedOperators() of OperandPort returns correct list of operators', function () {
247 | expect(evaluators._OperandPort.uid).to.be.equal('port');
248 | expect(evaluators._OperandPort.supportedOperators).to.deep.equal(['is', 'is_not']);
249 | });
250 | });
251 |
252 | describe('OperandExtension', function () {
253 | it('supportedOperators() of OperandExtension returns correct list of operators', function () {
254 | expect(evaluators._OperandExtension.uid).to.be.equal('extension');
255 | expect(evaluators._OperandExtension.supportedOperators).to.deep.equal(['is', 'is_not']);
256 | });
257 | });
258 |
259 | describe('OperandUrl', function () {
260 | it('supportedOperators() of OperandUrl returns correct list of operators', function () {
261 | expect(evaluators._OperandUrL.uid).to.be.equal('url');
262 | expect(evaluators._OperandUrL.supportedOperators).to.deep.equal(['regular_expression']);
263 | });
264 | });
265 |
266 | describe('getConditionOperands', function () {
267 | it('getConditionOperators() should return mapping of operands classes', function () {
268 | let ops = evaluators.getConditionOperands();
269 | let keys = ['host', 'scheme', 'path', 'port', 'url'];
270 | expect(ops).to.be.an('object');
271 | expect(ops).to.have.all.keys(keys);
272 | });
273 | });
274 | });
275 |
276 | describe('Operators', function () {
277 | describe('OperatorIs', function () {
278 | it('evaluate() should check values equality', function () {
279 | let operatorIs = new evaluators._OperatorIs(TEST_STRING);
280 | expect(operatorIs.evaluate('not test')).to.be.false;
281 | expect(operatorIs.evaluate('test')).to.be.true;
282 | expect(operatorIs.evaluate(' test ')).to.be.true;
283 | expect(operatorIs.evaluate('TeSt')).to.be.true;
284 | });
285 | });
286 |
287 | describe('OperatorIsNot', function () {
288 | it('evaluate() should check values inequality', function () {
289 | let operatorIsNot = new evaluators._OperatorIsNot(TEST_STRING);
290 | expect(operatorIsNot.evaluate('not test')).to.be.true;
291 | expect(operatorIsNot.evaluate('test')).to.be.false;
292 | expect(operatorIsNot.evaluate(' test ')).to.be.false;
293 | expect(operatorIsNot.evaluate('TeSt')).to.be.false;
294 | });
295 | });
296 |
297 | describe('OperatorContains', function () {
298 | it('evaluate() should check value contains string', function () {
299 | let operatorContains = new evaluators._OperatorContains(TEST_STRING);
300 | expect(operatorContains.evaluate('not test')).to.be.true;
301 | expect(operatorContains.evaluate(' test ')).to.be.true;
302 | expect(operatorContains.evaluate('TeSt')).to.be.true;
303 | expect(operatorContains.evaluate('nottest')).to.be.true;
304 | expect(operatorContains.evaluate('tteest')).to.be.false;
305 | });
306 | });
307 |
308 | describe('OperatorNotContains', function () {
309 | it('evaluate() should check value does not contain string', function () {
310 | let operatorNotContains = new evaluators._OperatorNotContains(TEST_STRING);
311 | expect(operatorNotContains.evaluate('not test')).to.be.false;
312 | expect(operatorNotContains.evaluate(' test ')).to.be.false;
313 | expect(operatorNotContains.evaluate('TeSt')).to.be.false;
314 | expect(operatorNotContains.evaluate('nottest')).to.be.false;
315 | expect(operatorNotContains.evaluate('tteest')).to.be.true;
316 | });
317 | });
318 |
319 | describe('OperatorStartsWith', function () {
320 | it('evaluate() should check value starts with string', function () {
321 | let operatorStartsWith = new evaluators._OperatorStartsWith(TEST_STRING);
322 | expect(operatorStartsWith.evaluate('not test')).to.be.false;
323 | expect(operatorStartsWith.evaluate('this is test')).to.be.false;
324 | expect(operatorStartsWith.evaluate('test this')).to.be.true;
325 | expect(operatorStartsWith.evaluate('TeSt this')).to.be.true;
326 | expect(operatorStartsWith.evaluate('testing')).to.be.true;
327 | expect(operatorStartsWith.evaluate(' testing')).to.be.true;
328 | });
329 | });
330 |
331 | describe('OperatorNotStartsWith', function () {
332 | it('evaluate() should check value does not start with string', function () {
333 | let operatorNotStartsWith = new evaluators._OperatorNotStartsWith(TEST_STRING);
334 | expect(operatorNotStartsWith.evaluate('not test')).to.be.true;
335 | expect(operatorNotStartsWith.evaluate('this is test')).to.be.true;
336 | expect(operatorNotStartsWith.evaluate('test this')).to.be.false;
337 | expect(operatorNotStartsWith.evaluate('TeSt this')).to.be.false;
338 | expect(operatorNotStartsWith.evaluate('testing')).to.be.false;
339 | expect(operatorNotStartsWith.evaluate(' testing')).to.be.false;
340 | });
341 | });
342 |
343 | describe('OperatorEndsWith', function () {
344 | it('evaluate() should check value ends with string', function () {
345 | let operatorEndsWith = new evaluators._OperatorEndsWith(TEST_STRING);
346 | expect(operatorEndsWith.evaluate('this is test')).to.be.true;
347 | expect(operatorEndsWith.evaluate('another test ')).to.be.true;
348 | expect(operatorEndsWith.evaluate('test done')).to.be.false;
349 | expect(operatorEndsWith.evaluate('tests')).to.be.false;
350 | expect(operatorEndsWith.evaluate('testing')).to.be.false;
351 | expect(operatorEndsWith.evaluate('tester')).to.be.false;
352 | });
353 | });
354 |
355 | describe('OperatorNotEndsWith', function () {
356 | it('evaluate() should check value ends with string', function () {
357 | let operatorNotEndsWith = new evaluators._OperatorNotEndsWith(TEST_STRING);
358 | expect(operatorNotEndsWith.evaluate('this is test')).to.be.false;
359 | expect(operatorNotEndsWith.evaluate('another test ')).to.be.false;
360 | expect(operatorNotEndsWith.evaluate('test done')).to.be.true;
361 | expect(operatorNotEndsWith.evaluate('tests')).to.be.true;
362 | expect(operatorNotEndsWith.evaluate('testing')).to.be.true;
363 | expect(operatorNotEndsWith.evaluate('tester')).to.be.true;
364 | });
365 | });
366 |
367 | describe('OperatorRegexp', function () {
368 | it('evaluate() should check value ends with string', function () {
369 | // eslint-disable-next-line no-useless-escape
370 | let operatorRegexp = new evaluators._OperatorRegexp('^(http|https)\:\/\/\\w+\.');
371 | expect(operatorRegexp.evaluate('http://google.com')).to.be.true;
372 | expect(operatorRegexp.evaluate('https://google.com')).to.be.true;
373 | expect(operatorRegexp.evaluate('ftp://mozzila.com')).to.be.false;
374 | });
375 | });
376 |
377 | describe('getConditionOperators', function () {
378 | it('getConditionOperators() should return mapping of operators classes', function () {
379 | let ops = evaluators.getConditionOperators();
380 | let keys = ['is', 'contains', 'starts_with', 'ends_with', 'is_not',
381 | 'not_contains', 'not_starts_with', 'not_ends_with', 'regular_expression'];
382 | expect(ops).to.be.an('object');
383 | expect(ops).to.have.all.keys(keys);
384 | });
385 | });
386 | });
387 |
388 | describe('RuleEvaluator', function () {
389 | it('_evaluateCondition() should return boolean', function () {
390 | let conditionToEvaluate = {
391 | text: 'google.com',
392 | operand: 'host',
393 | operator: 'is'
394 | };
395 | let objectToEvaluate = {
396 | host: 'notgoogle.com',
397 | scheme: 'https:',
398 | path: '/',
399 | port: '443'
400 | };
401 | let RuleEvaluator = new evaluators._RuleEvaluator({});
402 |
403 | expect(RuleEvaluator._evaluateCondition(conditionToEvaluate, objectToEvaluate)).to.be.false;
404 |
405 | objectToEvaluate['host'] = conditionToEvaluate['text'];
406 | expect(RuleEvaluator._evaluateCondition(conditionToEvaluate, objectToEvaluate)).to.be.true;
407 | });
408 |
409 | it('_any() should return true if at least one of the rule conditions match the evaluated object', function () {
410 | let objectToEvaluate = {
411 | host: 'console.aws.amazon.com',
412 | scheme: 'https:',
413 | path: '/',
414 | port: '443'
415 | };
416 | let RuleEvaluator = new evaluators._RuleEvaluator(RULE_SIMPLE_ANY_CONDITION);
417 | expect(RuleEvaluator._any(objectToEvaluate)).to.be.true;
418 |
419 | objectToEvaluate['host'] = 'facebook.com';
420 | expect(RuleEvaluator._any(objectToEvaluate)).to.be.false;
421 | });
422 |
423 | it('_all() should return true if all of the rule conditions match the evaluated object', function () {
424 | let objectToEvaluate = {
425 | host: 'github.com',
426 | scheme: 'https:',
427 | path: '/andriyko',
428 | port: '443'
429 | };
430 | let RuleEvaluator = new evaluators._RuleEvaluator(RULE_SIMPLE_ALL_CONDITIONS);
431 | expect(RuleEvaluator._all(objectToEvaluate)).to.be.true;
432 |
433 | objectToEvaluate['path'] = '/notandriyko';
434 | expect(RuleEvaluator._all(objectToEvaluate)).to.be.false;
435 | });
436 |
437 | it('evaluate() should use _all() or _any() based on the rule operator', function () {
438 | let objectToEvaluate = {
439 | host: 'github.com',
440 | scheme: 'https:',
441 | path: '/andriyko',
442 | port: '443'
443 | };
444 | let RuleEvaluator = new evaluators._RuleEvaluator(RULE_SIMPLE_ALL_CONDITIONS);
445 | expect(RuleEvaluator.evaluate(objectToEvaluate)).to.be.true;
446 | });
447 | });
448 |
449 | describe('crateObjectToEvaluate', function () {
450 | it('crateObjectToEvaluate() should parse URL and transform it to object', function () {
451 | let expectedResult = {
452 | host: 'google.com',
453 | scheme: 'https:',
454 | path: '/',
455 | port: '443'
456 | };
457 | let result = evaluators.crateObjectToEvaluate('https://google.com:443');
458 | for (let k of Object.keys(expectedResult)) {
459 | expect(result[k]).to.be.equal(expectedResult[k]);
460 | }
461 | });
462 | });
463 |
464 | describe('evaluateRule', function () {
465 | it('evaluateRule() should return boolean based on the rule evaluation', function () {
466 | let objectToEvaluate = {
467 | host: 'github.com',
468 | scheme: 'https:',
469 | path: '/andriyko',
470 | port: '443'
471 | };
472 | let result = evaluators.evaluateRule(RULE_SIMPLE_ALL_CONDITIONS, objectToEvaluate);
473 | expect(result).to.be.true;
474 |
475 | objectToEvaluate['path'] = 'notandriyko';
476 | result = evaluators.evaluateRule(RULE_SIMPLE_ALL_CONDITIONS, objectToEvaluate);
477 | expect(result).to.be.false;
478 | });
479 | });
480 |
481 | describe('evaluateRules', function () {
482 | it('evaluateRules() should evaluate object against all the rules', function () {
483 | let rule = evaluators.evaluateRules(RULES, 'https://github.com/andriyko');
484 | expect(rule._id).to.be.equal(RULE_SIMPLE_ALL_CONDITIONS._id);
485 |
486 | rule = evaluators.evaluateRules(RULES, 'https://github.com/notandriyko');
487 | expect(rule).to.be.equal(null);
488 | });
489 | });
490 |
--------------------------------------------------------------------------------
/tests/unit/open2.test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai');
2 | const expect = chai.expect; // we are using the "expect" style of Chai
3 | const proxyquire = require('proxyquire');
4 | const open2 = proxyquire('../.././src/app/js/open2', {
5 | 'child_process': {
6 | exec: function (cmd) { return cmd; }
7 | }
8 | });
9 |
10 | describe('open2', function () {
11 | it('open2() opens URL in a browser using its bundle identifier', function () {
12 | let result = open2('http://google.com', 'com.google.Chrome', '-b');
13 | expect(result).to.be.equal('open -b "com.google.Chrome" "http://google.com"');
14 | });
15 |
16 | it('open2() opens URL in a browser using its app identifier', function () {
17 | let result = open2('http://google.com', 'firefox', '-a');
18 | expect(result).to.be.equal('open -a "firefox" "http://google.com"');
19 | });
20 |
21 | it('open2() opens URL in a browser with a set of options', function () {
22 | let result = open2('http://google.com', 'com.google.Chrome', '-b', '-n -F');
23 | expect(result).to.be.equal('open -b "com.google.Chrome" -n -F "http://google.com"');
24 | });
25 |
26 | it('open2() opens URL in a browser with a set of additional arguments', function () {
27 | let result = open2('http://google.com', 'firefox', '-a', null, '-P "testProfile"');
28 | expect(result).to.be.equal('open -a "firefox" "http://google.com" −−args -P "testProfile"');
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/tests/unit/utils.test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai');
2 | const expect = chai.expect; // we are using the "expect" style of Chai
3 | const utils = require('../.././src/app/js/utils');
4 | const path = require('path');
5 |
6 | describe('Get apps details', function () {
7 | it('getAppInfo() returns correct app details', function () {
8 | let appPath = path.resolve(path.join(__dirname, '..', 'test_data', 'Applications', 'Safari.app'));
9 | let expectedAppInfo = {
10 | 'display_name': 'Safari',
11 | 'executable': 'Safari',
12 | 'icns': path.join(appPath, 'Contents/Resources/compass.icns'),
13 | 'identifier': 'com.apple.Safari',
14 | 'is_browser': true,
15 | 'name': 'Safari',
16 | 'path': appPath,
17 | 'schemes': ['http']
18 | };
19 | let result = utils.getAppInfo(appPath);
20 | expect(result).to.deep.equal(expectedAppInfo);
21 | });
22 |
23 | it('getApps() returns correct list of apps and ignores Calculator.app', function () {
24 | let appsRoot = path.resolve(path.join(__dirname, '..', 'test_data', 'Applications'));
25 | let expectedResult = [
26 | {
27 | 'display_name': 'Google Chrome',
28 | 'executable': 'Google Chrome',
29 | 'icns': path.join(appsRoot, 'Google Chrome.app/Contents/Resources/app.icns'),
30 | 'identifier': 'com.google.Chrome',
31 | 'is_browser': true,
32 | 'name': 'Chrome',
33 | 'path': path.join(appsRoot, 'Google Chrome.app'),
34 | 'schemes': ['http']
35 | },
36 | {
37 | 'display_name': 'Safari',
38 | 'executable': 'Safari',
39 | 'icns': path.join(appsRoot, 'Safari.app/Contents/Resources/compass.icns'),
40 | 'identifier': 'com.apple.Safari',
41 | 'is_browser': true,
42 | 'name': 'Safari',
43 | 'path': path.join(appsRoot, 'Safari.app'),
44 | 'schemes': ['http']
45 | }
46 | ];
47 | let result = utils.getApps(appsRoot);
48 | expect(result).to.deep.equal({errors: [], results: expectedResult});
49 | });
50 | });
51 |
--------------------------------------------------------------------------------