├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── LICENSE
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── app
├── components
│ ├── command-palette
│ │ ├── command-palette.html
│ │ └── command-palette.scss
│ ├── popup.html
│ ├── static-command-palette
│ │ ├── index.js
│ │ ├── static-command-palette.html
│ │ └── static-command-palette.scss
│ └── theme-customizer
│ │ ├── index.js
│ │ ├── theme-customizer.html
│ │ └── theme-customizer.scss
├── images
│ ├── about-icon.svg
│ ├── amazon-icon.svg
│ ├── blank-page-icon.svg
│ ├── calculator-icon.svg
│ ├── cato-logo.png
│ ├── cato-logo128.png
│ ├── cato-logo16.png
│ ├── cato-logo48.png
│ ├── cato-white-logo@2x.png
│ ├── chrome-icon.svg
│ ├── clifford-headshot.jpg
│ ├── firefox-icon.svg
│ ├── github-icon.svg
│ ├── gmail-icon.svg
│ ├── google-calendar-icon.svg
│ ├── google-drive-icon.svg
│ ├── google-maps-icon.svg
│ ├── google-search-icon.svg
│ ├── incognito-icon.svg
│ ├── shortened-url-icon.svg
│ ├── theming-icon.svg
│ ├── twitter-icon.svg
│ ├── white-chrome-icon.svg
│ └── youtube-icon.svg
├── manifest.json
├── pages
│ ├── options
│ │ ├── options.html
│ │ ├── options.js
│ │ └── options.scss
│ └── popup
│ │ ├── popup.html
│ │ ├── popup.js
│ │ └── popup.scss
├── plugins
│ ├── close-tab
│ │ └── index.js
│ ├── close-tabs-except-active
│ │ └── index.js
│ ├── copy-url
│ │ └── index.js
│ ├── detach-tab
│ │ └── index.js
│ ├── disable-extension
│ │ └── index.js
│ ├── disable-extensions
│ │ └── index.js
│ ├── duplicate-tab-incognito
│ │ └── index.js
│ ├── duplicate-tab
│ │ └── index.js
│ ├── enable-extension
│ │ └── index.js
│ ├── fallback-web-searches
│ │ └── index.js
│ ├── find-bookmark
│ │ └── index.js
│ ├── find-tab
│ │ └── index.js
│ ├── merge-sort-tabs
│ │ └── index.js
│ ├── move-tab-left
│ │ └── index.js
│ ├── move-tab-right
│ │ └── index.js
│ ├── next-tab
│ │ └── index.js
│ ├── open-about
│ │ └── index.js
│ ├── open-amazon
│ │ └── index.js
│ ├── open-app-settings
│ │ └── index.js
│ ├── open-apps
│ │ └── index.js
│ ├── open-bookmarks
│ │ └── index.js
│ ├── open-downloads
│ │ └── index.js
│ ├── open-extensions
│ │ └── index.js
│ ├── open-gmail
│ │ └── index.js
│ ├── open-google-calendar
│ │ └── index.js
│ ├── open-google-drive
│ │ └── index.js
│ ├── open-history
│ │ └── index.js
│ ├── open-incognito
│ │ └── index.js
│ ├── open-new-tab
│ │ └── index.js
│ ├── open-settings
│ │ └── index.js
│ ├── open-window
│ │ └── index.js
│ ├── page-back
│ │ └── index.js
│ ├── page-forward
│ │ └── index.js
│ ├── plugins.js
│ ├── previous-tab
│ │ └── index.js
│ ├── print-tab
│ │ └── index.js
│ ├── reload-tab
│ │ └── index.js
│ ├── reload-tabs
│ │ └── index.js
│ ├── shorten-url
│ │ └── index.js
│ ├── show-all-commands
│ │ └── index.js
│ ├── sort-tabs
│ │ └── index.js
│ ├── toggle-bookmark
│ │ └── index.js
│ ├── toggle-fullscreen-mode
│ │ └── index.js
│ ├── toggle-mute-tab
│ │ └── index.js
│ ├── toggle-mute-tabs
│ │ └── index.js
│ ├── uninstall-extension
│ │ └── index.js
│ ├── video-controller
│ │ └── index.js
│ ├── view-source-toggle
│ │ └── index.js
│ └── view-source
│ │ └── index.js
└── util.js
├── media
├── cato-logo.png
├── change-shortcut-how-to.png
├── chrome-icon.png
├── example-calculator.png
├── example-change-tab.png
├── example-command-showcase2.png
├── example-commands-showcase1.png
├── example-disable-extension.png
├── example-enable-extension.png
├── example-fallback-search.png
├── example-theme-customizer.png
├── feature-banner.png
├── opera-icon.png
└── usage-example.gif
├── package-lock.json
├── package.json
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain
2 | # consistent coding styles between different editors and IDEs.
3 |
4 | root = true
5 |
6 | [*]
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | !/*.js
2 | /docs/lib/
3 | /tests/**/*.js
4 | !/tests/**/jsfmt.spec.js
5 | /test.js
6 | /dist/
7 | /scripts/build
8 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = {
4 | env: {
5 | browser: true,
6 | es6: true,
7 | node: true
8 | },
9 | extends: ["eslint:recommended"],
10 | "globals": {
11 | "chrome": true
12 | },
13 | parserOptions: {
14 | ecmaVersion: 6,
15 | ecmaFeatures: {
16 | jsx: true
17 | }
18 | },
19 | rules: {
20 | "accessor-pairs": "error",
21 | "array-bracket-spacing": "error",
22 | "array-callback-return": "error",
23 | "arrow-parens": ["error", "always"],
24 | "arrow-spacing": ["error", {
25 | "after": true,
26 | "before": true
27 | }],
28 | "block-scoped-var": "error",
29 | "block-spacing": "error",
30 | "brace-style": ["error", "stroustrup"],
31 | "comma-dangle": "error",
32 | "comma-spacing": ["error", {
33 | "after": true,
34 | "before": false
35 | }],
36 | "comma-style": ["error", "last"],
37 | "computed-property-spacing": "error",
38 | "consistent-this": ["error", "that"],
39 | "curly": "error",
40 | "default-case": "error",
41 | "dot-location": ["error", "property"],
42 | "dot-notation": ["error", {
43 | "allowKeywords":
44 | true
45 | }],
46 | "eqeqeq": "error",
47 | "func-call-spacing": "error",
48 | "generator-star-spacing": "error",
49 | "global-require": "error",
50 | "guard-for-in": "error",
51 | "handle-callback-err": "error",
52 | "id-blacklist": "error",
53 | "id-length": "error",
54 | "id-match": "error",
55 | "indent": "off",
56 | "init-declarations": "off",
57 | "jsx-quotes": "error",
58 | "key-spacing": "error",
59 | "keyword-spacing": "off",
60 | "linebreak-style": ["error", "unix"],
61 | "lines-around-comment": "error",
62 | "lines-around-directive": "error",
63 | "max-depth": "error",
64 | "max-len": "off",
65 | "max-nested-callbacks": "error",
66 | "max-params": ["error", 4],
67 | "max-statements-per-line": "error",
68 | "new-cap": "error",
69 | "new-parens": "error",
70 | "newline-per-chained-call": "error",
71 | "no-caller": "error",
72 | "no-console": "off",
73 | "no-confusing-arrow": "error",
74 | "no-duplicate-imports": "error",
75 | "no-else-return": "error",
76 | "no-empty-function": "error",
77 | "no-eq-null": "error",
78 | "no-extend-native": "error",
79 | "no-extra-parens": "error",
80 | "no-floating-decimal": "error",
81 | "no-implicit-coercion": "error",
82 | "no-implicit-globals": "error",
83 | "no-implied-eval": "error",
84 | "no-inner-declarations": "off",
85 | "no-invalid-this": "error",
86 | "no-label-var": "error",
87 | "no-labels": "error",
88 | "no-lone-blocks": "error",
89 | "no-lonely-if": "error",
90 | "no-loop-func": "error",
91 | "no-magic-numbers": "off",
92 | "no-mixed-operators": "off",
93 | "no-mixed-requires": "error",
94 | "no-multi-assign": "off",
95 | "no-multi-spaces": "error",
96 | "no-multi-str": "error",
97 | "no-native-reassign": "error",
98 | "no-negated-condition": "error",
99 | "no-negated-in-lhs": "error",
100 | "no-nested-ternary": "error",
101 | "no-new": "error",
102 | "no-new-func": "error",
103 | "no-new-object": "error",
104 | "no-new-require": "error",
105 | "no-new-wrappers": "error",
106 | "no-octal-escape": "error",
107 | "no-param-reassign": "error",
108 | "no-path-concat": "error",
109 | "no-plusplus": "error",
110 | "no-process-exit": "error",
111 | "no-proto": "error",
112 | "no-prototype-builtins": "error",
113 | "no-restricted-globals": "error",
114 | "no-restricted-imports": "error",
115 | "no-restricted-modules": "error",
116 | "no-restricted-properties": "error",
117 | "no-restricted-syntax": "error",
118 | "no-return-assign": "error",
119 | "no-return-await": "error",
120 | "no-script-url": "error",
121 | "no-self-compare": "error",
122 | "no-sequences": "error",
123 | "no-shadow": "error",
124 | "no-shadow-restricted-names": "error",
125 | "no-spaced-func": "error",
126 | "no-sync": "error",
127 | "no-tabs": "error",
128 | "no-template-curly-in-string": "error",
129 | "no-throw-literal": "off",
130 | "no-trailing-spaces": "error",
131 | "no-undef-init": "error",
132 | "no-undefined": "error",
133 | "no-underscore-dangle": "error",
134 | "no-unmodified-loop-condition": "error",
135 | "no-unneeded-ternary": "error",
136 | "no-unused-expressions": "off",
137 | "no-useless-call": "error",
138 | "no-useless-computed-key": "error",
139 | "no-useless-concat": "error",
140 | "no-useless-constructor": "error",
141 | "no-useless-escape": "error",
142 | "no-useless-rename": "error",
143 | "no-useless-return": "error",
144 | "no-var": "error",
145 | "no-void": "error",
146 | "no-warning-comments": "error",
147 | "no-whitespace-before-property": "error",
148 | "no-with": "error",
149 | "object-curly-spacing": ["error", "never"],
150 | "one-var": "off",
151 | "one-var-declaration-per-line": "error",
152 | "prefer-const": "error",
153 | "prefer-numeric-literals": "error",
154 | "prefer-promise-reject-errors": "error",
155 | "prefer-reflect": "error",
156 | "prefer-rest-params": "error",
157 | "prefer-spread": "error",
158 | "prefer-template": "error",
159 | "quote-props": "off",
160 | "quotes": "off",
161 | "radix": "error",
162 | "require-await": "error",
163 | "require-jsdoc": ["error", {
164 | "require": {
165 | "ArrowFunctionExpression": false,
166 | "ClassDeclaration": true,
167 | "FunctionDeclaration": true,
168 | "MethodDefinition": true
169 | }
170 | }],
171 | "rest-spread-spacing": "error",
172 | "semi": "off",
173 | "semi-spacing": "error",
174 | "space-before-blocks": "error",
175 | "space-before-function-paren": "off",
176 | "space-in-parens": ["error", "never"],
177 | "space-infix-ops": "error",
178 | "space-unary-ops": "error",
179 | "template-curly-spacing": ["error", "never"],
180 | "template-tag-spacing": "error",
181 | "unicode-bom": ["error", "never"],
182 | "valid-jsdoc": [
183 | "error", {
184 | "prefer": {
185 | "arg": "param",
186 | "argument": "param",
187 | "class": "constructor",
188 | "return": "returns",
189 | "virtual": "abstract"
190 | },
191 | "requireParamDescription": false,
192 | "requireReturnDescription": false
193 | }],
194 | "yoda": ["error", "never"]
195 | }
196 | };
197 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project related
2 | extension
3 |
4 | ### node etc ###
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 |
16 | #For ionic build
17 | platforms/
18 | www/lib
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Compiled Dirs (http://nodejs.org/api/addons.html)
30 | build/
31 |
32 | # Dependency directorys
33 | # Deployed apps should consider commenting these lines out:
34 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
35 | node_modules/
36 | bower_components/
37 | package-lock.json
38 |
39 |
40 | # Packages #
41 | ############
42 | # it's better to unpack these files and commit the raw source
43 | # git has its own built in compression methods
44 | *.7z
45 | *.dmg
46 | *.gz
47 | *.iso
48 | *.jar
49 | *.rar
50 | *.tar
51 | *.zip
52 |
53 |
54 |
55 | # OS generated files #
56 | ######################
57 | .DS_Store
58 | .DS_Store?
59 | ._*
60 | .Spotlight-V100
61 | .Trashes
62 | ehthumbs.db
63 | Thumbs.db
64 |
65 | # Text Editors
66 | *.vscode
67 | *.sublime
68 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Attribution
36 |
37 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
38 |
39 | [homepage]: http://contributor-covenant.org
40 | [version]: http://contributor-covenant.org/version/1/4/
41 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Cato
2 |
3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4 |
5 | The following is a set of guidelines for contributing to [Cato](https://github.com/cliffordfajardo/cato). These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
6 |
7 |
8 |
9 | ### Before Submitting an Issue
10 |
11 | First, please do a search in open issues to see if the issue or feature request has already been filed. If you find your issue already exists, make relevant comments and add your reaction. Use a reaction in place of a "+1" comment.
12 |
13 | - 👍 upvote
14 |
15 | - 👎 downvote
16 |
17 |
18 | ### Browser Extension Development Resources
19 |
20 | - [Chrome Extension Developer Guide](https://developer.chrome.com/extensions/devguide)
21 | - When you developing for this repo, please use the `browser` object for commands instead of `chrome` like the other commands in the `/plugins` folder . This allows us to start writing cross-browser extension code that [maybe compatible with other browsers](https://developer.mozilla.org/en-US/Add-ons/WebExtensions) like Firefox in the future.
22 |
23 |
24 | ### Brief Project Structure Overview
25 |
26 | All of Cato's commands comes from the `/plugins` folder. Essentially, each command has it's own folder and `index.js` file. Currently, each command is exported as a single Object.
27 |
28 | `plugins/plugins.js` is where all all of the commands get loaded into a single
29 | array and `popup.js` is where they are eventually used.
30 |
31 | **If you'd like to contribute a new command** , please create a new folder inside `plugins/`. Currently this project uses [async await](https://hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9) to deal with asynchronous code. When and if you can, please try and use async/await instead of callbacks and promises.
32 |
33 |
34 | ## Installing Prerequisites
35 |
36 | You'll need git and a recent version of Node.JS (any v7.2.1+ is recommended with npm v3.10.10+). nvm is also highly recommended.
37 |
38 | ## Development
39 |
40 | Run `npm install` to install all dependencies for the project.
41 |
42 | To run the extension locally, you'll need to build it with:
43 |
44 | ```sh
45 | # Build once
46 | npm run build
47 | ```
48 |
49 | ```sh
50 | # Build every time a file changes
51 | npm run watch
52 | ```
53 |
54 | Once built, load it in the browser.
55 |
56 | ### Chrome
57 |
58 | 1. Visit `chrome://extensions/` in Chrome
59 | 2. Enable the **Developer mode**
60 | 3. Click on **Load unpacked extension**
61 | 4. Select the folder `extension`
62 |
63 |
64 |
65 | ## Tips
66 |
67 | Once you load the extension, invoke Cato by pressing the icon or command shortcut, you can option click
68 | the popup window and that will bring up the chrome developer tools for inspecting and debugging.
69 |
70 | Consider using `debugger` statements in your code. They're useful when you're developing browser extensions.
71 |
72 |
73 |
74 | ### Pull Requests
75 |
76 | - Do not include issue numbers in the PR title
77 | - Include screenshots and animated GIFs in your pull request whenever possible.
78 | - End all files with a newline
79 |
80 | To enable us to quickly review and accept your pull requests, always create one pull request per issue and link the issue in the pull request. Never merge multiple requests in one unless they have the same root cause. Be sure to follow our Coding Guidelines and keep code changes as small as possible. Avoid pure formatting changes to code that has not been modified otherwise. Pull requests should contain tests whenever possible.
81 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | ### Prerequisites
8 |
9 | * [ ] Put an X between the brackets on this line if you have done all of the following:
10 | * Checked that your issue isn't already filed: https://github.com/cliffordfajardo/cato/issues
11 |
12 |
13 | ### Description
14 |
15 | [Description of the issue]
16 |
17 | ### Steps to Reproduce (including a GIF is helpful)
18 |
19 | 1. [First Step]
20 | 2. [Second Step]
21 | 3. [and so on...]
22 |
23 | **Expected behavior:** [What you expect to happen]
24 |
25 | **Actual behavior:** [What actually happens]
26 |
27 | **Reproduces how often:** [What percentage of the time does it reproduce?]
28 |
29 |
30 | ### Versions & Operating System of Browser
31 |
32 | - Version:
33 | - Operating System:
34 |
35 |
36 |
37 | ### Additional Information
38 |
39 | Any additional information, configuration or data that might be necessary to reproduce the issue.
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2017 Clifford Fajardo
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Requirements
2 |
3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.
4 | * All new code requires tests to ensure against regressions
5 |
6 | ### Description of the Change
7 |
8 |
13 |
14 | ### Alternate Designs
15 |
16 |
17 |
18 | ### Why Should This Be In Core?
19 |
20 |
21 |
22 | ### Benefits
23 |
24 |
25 |
26 | ### Possible Drawbacks
27 |
28 |
29 |
30 | ### Applicable Issues
31 |
32 |
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cato
2 |
3 | [![Chrome version][badge-cws]][link-cws]
4 | [![PRs Welcome][prs-badge]][prs]
5 | [![MIT License][license-badge]][LICENSE]
6 | [![Tweet][twitter-badge]][twitter]
7 |
8 | > Cato - a browser extension that allows you to control your browser with simple commands.
9 |
10 | **Discuss it on [Product Hunt](https://www.producthunt.com/posts/cato)** 🐈
11 |
12 |
13 | ## Install
14 |
15 | This browser extension available for:
16 |
17 |
18 |
19 |
20 | Install
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Chrome
35 |
36 |
37 | Opera
38 |
39 |
40 |
41 |
42 |
43 |
44 | ## Highlights
45 |
46 |
47 |
48 |
49 | Sample Commands
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Fallback Searches and Calculator
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | Customize your launcher
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | ### Changing this extension's shortcut keys
95 |
96 | - By default to open Cato you can click the browser icon near your URL bar or you can use:
97 | - Mac: Command ⌘ + J
98 | - Windows and Linux: Ctrl + J
99 | - Alternatively, in Google Chrome, you can go to the URL `chrome://extensions/shortcuts` and scroll to the bottom and click **Keyboard shortcuts**
100 |
101 |
102 |
103 | ## Contributing and Development
104 |
105 | We're happy to receive suggestions and contributions. Please take a look at [CONTRIBUTING.md](CONTRIBUTING.md) to learn more about how to submit code, report bugs and so on.
106 |
107 |
108 |
109 |
110 | ## Story Behind Cato
111 |
112 | One day, I forgot my laptop at home while visiting a cousin's house. Luckily, he had a backup chromebook laptop I could use temporarily. Of course using this pc wasn't the same as my own laptop which had Alfred App, my text editors and a few other command-driven apps.
113 |
114 | I'd gotten accustomed to simply typing everything I needed on my computer, without almost ever using my mouse. I wanted a similar command-driven experience in one of the apps I use most as a software developer - my web browser. Thus, Cato was born.
115 |
116 | Now, with Cato, I can get nearly the same command-driven experiences that apps like Alfred App/Atom/VSCode/Spotlight have, but in my web browser on nearly any operating system! #yay 🎉🍻🎊
117 |
118 |
119 |
120 | [badge-cws]: https://img.shields.io/chrome-web-store/v/hlepfoohegkhhmjieoechaddaejaokhf.svg?label=chrome
121 | [link-cws]: https://chrome.google.com/webstore/detail/cato/icphdcfpompgbdikholnedfeidemgobg "Version published on Chrome Web Store"
122 |
123 | [license-badge]: https://img.shields.io/npm/l/cross-env.svg?style=flat-square
124 | [license]: https://github.com/kentcdodds/cross-env/blob/master/other/LICENSE
125 |
126 | [license-badge]: https://img.shields.io/npm/l/cross-env.svg?style=flat-square
127 | [license]: https://github.com/kentcdodds/cross-env/blob/master/other/LICENSE
128 |
129 | [prs]: http://makeapullrequest.com
130 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
131 |
132 | [twitter]: https://twitter.com/intent/tweet?url=https%3A%2F%2Fchrome.google.com%2Fwebstore%2Fdetail%2Fcato%2Ficphdcfpompgbdikholnedfeidemgobg&via=cliffordfajard0&text=Checkout%20the%20command%20launcher%20browser%20extension%20-%20Cato%21
133 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/kentcdodds/cross-env.svg?style=social
134 |
135 |
136 |
137 | ### Thank You
138 | > Thanks checking out my extension! I'm mostly hang out on [twitter](https://twitter.com/cliffordfajard0), ask me questions or let's talk!
139 |
140 |
141 |
--------------------------------------------------------------------------------
/app/components/command-palette/command-palette.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/components/command-palette/command-palette.scss:
--------------------------------------------------------------------------------
1 | //add bulma font module
2 |
3 | :root {
4 | //classic #FFFFFF is the default theme
5 | // element background colors
6 | --app-background-color: #FFFFFF;
7 | --search-input-caret-color: #222222;
8 | --search-results-scrollbar-color: #222222;
9 | --selected-suggestion-background-color: #222222;
10 | // text sizing
11 | --search-input-value-text-size: 30px;
12 | --suggestion-title-text-size: 16px;
13 | --suggestion-subtitle-text-size: 13px;
14 | //text colors
15 | --search-input-value-text-color: #222222;
16 | --selected-suggestion-title-text-color: #FFFFFF;
17 | --selected-suggestion-subtitle-text-color: #FFFFFF;
18 | --selected-suggestion-character-match-color: #FFFFFF;
19 | --suggestion-title-text-color: #222222;
20 | --suggestion-subtitle-text-color: #222222;
21 | --suggestion-character-match-color: #222222;
22 | //spacing
23 | --search-results-scrollbar-width: 2px;
24 | }
25 |
26 | .cLauncher {
27 | background: var(--app-background-color);
28 | min-height: 60px;
29 | width: var(--app-width-size);
30 |
31 | .cLauncher__search {
32 | background-color: var(--app-background-color);
33 | caret-color: var(--search-input-caret-color);
34 | border: none;
35 | color: var(--search-input-value-text-color);
36 | height: 50px;
37 | font-size: var(--search-input-value-text-size);
38 | font-weight: 300;
39 | width: 100%;
40 | padding: 5px 5px 5px 15px;
41 |
42 | &:focus {
43 | outline: none;
44 | }
45 | }
46 |
47 | .cLauncher__icon {
48 | margin-top: 5px;
49 | }
50 |
51 | .cLauncher__search-input-wrapper {
52 | display: flex;
53 | justify-content: space-between;
54 | padding: 5px;
55 | }
56 |
57 | .cLauncher__suggestions {
58 | max-height: 300px;
59 | margin: 0;
60 | background: var(--app-background-color);
61 | overflow-y: scroll;
62 | padding: 0;
63 | }
64 |
65 | .cLauncher__scrollbar::-webkit-scrollbar {
66 | width: var(--search-results-scrollbar-width);
67 | }
68 |
69 | .cLauncher__scrollbar::-webkit-scrollbar-thumb {
70 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
71 | background-color: var(--search-results-scrollbar-color);
72 | }
73 |
74 | .cLauncher__suggestion {
75 | display: flex;
76 | align-items: center;
77 | padding: 10px 10px 10px 20px;
78 | list-style: none;
79 | height: 60px;
80 |
81 | &.selected {
82 | background-color: var(--selected-suggestion-background-color);
83 |
84 | .cLauncher__suggestion-title {
85 | color: var(--selected-suggestion-title-text-color);
86 | }
87 |
88 | .cLauncher__suggestion-subtitle {
89 | color: var(--selected-suggestion-subtitle-text-color);
90 | }
91 |
92 | strong {
93 | color: var(--selected-suggestion-character-match-color);
94 | }
95 | }
96 |
97 | .cLauncher__suggestion-icon {
98 | width: 20px;
99 | margin-right: 10px;
100 | }
101 |
102 | .cLauncher__suggestion-text-info {
103 | display: flex;
104 | flex-direction: column;
105 | width: 80%;
106 | }
107 |
108 | .cLauncher__suggestion-title {
109 | color: var(--suggestion-title-text-color);
110 | font-size: var(--suggestion-title-text-size);
111 | margin-bottom: 2px;
112 | }
113 |
114 | .cLauncher__suggestion-subtitle {
115 | color: var(--suggestion-subtitle-text-color);
116 | font-size: var(--suggestion-subtitle-text-size);
117 | }
118 |
119 | .cLauncher__suggestion-subtitle,
120 | .cLauncher__suggestion-title {
121 | max-width: 400px;
122 | overflow: hidden;
123 | text-overflow: ellipsis;
124 | white-space: nowrap;
125 | }
126 | strong {
127 | font-weight: bold;
128 | color: var(--suggestion-character-match-color);
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/app/components/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | App
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/components/static-command-palette/index.js:
--------------------------------------------------------------------------------
1 | const {h} = require('dom-chef')
2 | const commandPaletteHTML = require('./static-command-palette.html')
3 | const commandPaletteElement = (
)
4 |
5 | module.exports = commandPaletteElement
6 |
--------------------------------------------------------------------------------
/app/components/static-command-palette/static-command-palette.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/components/static-command-palette/static-command-palette.scss:
--------------------------------------------------------------------------------
1 | .static-cLauncher {
2 | min-width: 500px;
3 | box-shadow: 0 6px 2px 0 rgba(0,0,0,.14), 0 7px 8px -2px rgba(0,0,0,.2), 0 1px 83px 0 rgba(0,0,0,4.12);
4 | margin-right: 60px;
5 | }
6 |
--------------------------------------------------------------------------------
/app/components/theme-customizer/index.js:
--------------------------------------------------------------------------------
1 | const {h} = require('dom-chef')
2 | const utils = require('../../util')
3 | const browser = require('webextension-polyfill')
4 |
5 | const themeCustomizerHTML = require('./theme-customizer.html')
6 | const themeCustomizerElement = (
)
7 | const resetThemeButton = themeCustomizerElement.querySelector('#reset-theme')
8 | const saveThemeButton = themeCustomizerElement.querySelector('#save-theme-settings')
9 |
10 | const htmlElement = document.querySelector('html')
11 | const themeControlInputs = themeCustomizerElement.querySelectorAll('.theme-controls__input')
12 |
13 | resetThemeButton.addEventListener('click', () => {
14 | utils.resetFakeAppTheme()
15 | utils.resetLocalStorageTheme()
16 | themeControlInputs.forEach((input) => utils.resetThemeInputValue(input))
17 | })
18 |
19 |
20 |
21 | themeControlInputs.forEach((input) => input.addEventListener('change', utils.handleThemeInputValueChanges))
22 |
23 | //default values
24 | themeControlInputs.forEach(async (input) => {
25 | const { themeConfig } = await browser.storage.sync.get('themeConfig')
26 | const property = input.dataset.cssvariable
27 | let value = themeConfig[property]
28 | if (input.type === 'number') {
29 | value = value.slice(0, -2) // 500px -> 500
30 | }
31 | input.setAttribute('value', value)
32 | })
33 |
34 | //set the static launcher's color to the values we have in local storage, if any.
35 | themeControlInputs.forEach(async (input) => {
36 | const {themeConfig} = await browser.storage.sync.get('themeConfig')
37 | for (const property in themeConfig){
38 | htmlElement.style.cssText += `${property}: ${themeConfig[property]}`
39 | }
40 |
41 | })
42 |
43 |
44 |
45 |
46 | module.exports = themeCustomizerElement
47 |
--------------------------------------------------------------------------------
/app/components/theme-customizer/theme-customizer.html:
--------------------------------------------------------------------------------
1 |
96 |
--------------------------------------------------------------------------------
/app/components/theme-customizer/theme-customizer.scss:
--------------------------------------------------------------------------------
1 | .theme-controls {
2 | width: 480px;
3 | background: #222222;
4 | padding: 25px;
5 | padding-top:10px;
6 | box-shadow: 0 6px 2px 0 rgba(0,0,0,.14), 0 7px 8px -2px rgba(0,0,0,.2), 0 1px 83px 0 rgba(0,0,0,4.12);
7 | }
8 |
9 | .theme-controls__group-title {
10 | color: white;
11 | border-bottom: 2px solid white;
12 | &:not(:first-child) {
13 | margin: 5px 0;
14 | }
15 | }
16 | .theme-controls__control-container {
17 | display: flex;
18 | justify-content: space-between;
19 | margin-bottom: -3px;
20 | }
21 | .theme-controls__label {
22 | font-size: 14px;
23 | color: white;
24 | display: inline-block;
25 | align-self: center;
26 | }
27 |
28 | .theme-controls__input_color-picker {
29 | background: none;
30 | border:none;
31 | border-image: none;
32 | width: 60px;
33 | height: 30px;
34 | border: 0;
35 | padding: 0;
36 | }
37 |
38 | .theme-controls__input_font-resizer {
39 | width: 60px;
40 | padding-left: 10px !important;
41 | height: 20px;
42 | padding: 0;
43 | margin-bottom: 6px;
44 | }
45 |
46 | .theme-controls__footer {
47 | display: flex;
48 | margin-top: 20px;
49 |
50 | .theme-controls__button {
51 | &:first-child {
52 | margin-right: 20px;
53 | }
54 | }
55 | }
56 |
57 |
58 | .theme-modal {
59 | width: 460px !important; //just add a more specific class..theme instead of appearance
60 | background: #222222;
61 | .theme-modal__title,
62 | .theme-modal__content {
63 | color: white;
64 | }
65 |
66 | .theme-modal__button{
67 | &.theme-modal__button_save {
68 | background: white;
69 | }
70 | &.theme-modal__button_cancel {
71 | background: hotpink;
72 | color: white;
73 | }
74 | }
75 |
76 | }
77 |
78 | .theme-controls-actions {
79 |
80 | .theme-controls__button {
81 | border:none;
82 | padding: 12px;
83 | border-radius: 2px;
84 |
85 | &_reset {
86 | background: white;
87 | }
88 | &_save {
89 | background: hotpink;
90 | color: white;
91 | }
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/app/images/about-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/images/amazon-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
82 |
83 |
--------------------------------------------------------------------------------
/app/images/blank-page-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/images/calculator-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
9 |
10 |
12 |
14 |
16 |
17 |
19 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
43 |
44 |
46 |
48 |
50 |
52 |
53 |
55 |
56 |
58 |
60 |
62 |
64 |
65 |
66 |
68 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/app/images/cato-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/app/images/cato-logo.png
--------------------------------------------------------------------------------
/app/images/cato-logo128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/app/images/cato-logo128.png
--------------------------------------------------------------------------------
/app/images/cato-logo16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/app/images/cato-logo16.png
--------------------------------------------------------------------------------
/app/images/cato-logo48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/app/images/cato-logo48.png
--------------------------------------------------------------------------------
/app/images/cato-white-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/app/images/cato-white-logo@2x.png
--------------------------------------------------------------------------------
/app/images/chrome-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
10 |
13 |
16 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/images/clifford-headshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/app/images/clifford-headshot.jpg
--------------------------------------------------------------------------------
/app/images/firefox-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/images/github-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/images/gmail-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
11 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/images/google-calendar-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
9 |
12 |
14 |
15 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/images/google-drive-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/images/google-maps-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
8 |
10 |
12 |
15 |
19 |
24 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/images/google-search-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
11 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/images/incognito-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
182 |
183 |
--------------------------------------------------------------------------------
/app/images/shortened-url-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
9 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/images/theming-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/images/twitter-icon.svg:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/images/white-chrome-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/images/youtube-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Cato",
4 | "description": "Cato - a command launcher for your web browser.",
5 | "homepage_url": "https://github.com/cliffordfajardo/cato",
6 | "version": "1.1.5",
7 |
8 | "browser_action": {
9 | "default_icon": "images/cato-logo.png",
10 | "default_popup": "popup.html",
11 | "default_title": "Cato"
12 | },
13 | "options_ui": {
14 | "page": "options.html",
15 | "open_in_tab": true
16 | },
17 | "background": {
18 | "scripts": [ "popup.js"],
19 | "persistent": false
20 | },
21 | "icons": {
22 | "16": "images/cato-logo16.png",
23 | "48": "images/cato-logo48.png",
24 | "128": "images/cato-logo128.png"
25 | },
26 | "permissions": [
27 | "activeTab",
28 | "bookmarks",
29 | "tabs",
30 | "management",
31 | "storage"
32 | ],
33 | "content_security_policy": "script-src 'self' https://www.google-analytics.com; object-src 'self'",
34 | "commands": {
35 | "_execute_browser_action": {
36 | "suggested_key": {
37 | "windows": "Ctrl+J",
38 | "mac": "Command+J",
39 | "chromeos": "Ctrl+M",
40 | "linux": "Ctrl+M"
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/pages/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cato - Options/Settings
6 |
7 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | What is Cato?
45 |
46 | Cato is an intelligent command launcher for your web browser. It aims to increase your productivity by allowing
47 | you to tell your browser what to do, not how to do it. The days of remembering keyboard shortcuts and excessive
48 | mouse use are over.
49 |
50 |
51 | Example Use
52 |
53 | VIDEO
54 | For more details about Cato visit this README page .
55 |
56 |
57 |
58 | Changing this extension's shortcut keys
59 | By default to open Cato you can click the browser icon near your URL bar or you can use:
60 |
61 | Mac: Command ⌘ + J
62 | Windows and Linux: Ctrl + J
63 | Alternatively, in Google Chrome, you can go to the URL chrome://extensions and scroll to the bottom and click Keyboard shortcuts
64 |
65 |
66 |
67 | Future Updates
68 |
69 | Cato is free and open source software.
70 | If you have any feedback or would like to contribute to the underlying code,
71 | visit the source repository here.
72 |
73 |
74 |
75 | Thanks for downloading Cato. If you found this browser extension useful, let me know by rating the extension
76 | or by
77 | sharing it with others. - Cheers 🎉
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/pages/options/options.js:
--------------------------------------------------------------------------------
1 | //Requiring the CSS we will use on this page.
2 | //Webpack will output `options.css` from all these requires
3 | require('./options.scss')
4 | const browser = require('webextension-polyfill')
5 | const themeCustomizerElement = require('../../components/theme-customizer')
6 | const staticCommandPaletteElement = require('../../components/static-command-palette')
7 | const utils = require('../../util.js')
8 | const Grapnel = require('grapnel')
9 | const router = new Grapnel()
10 |
11 |
12 | const appearancePageElement = document.querySelector('#page-theme')
13 | appearancePageElement.appendChild(staticCommandPaletteElement)
14 | appearancePageElement.appendChild(themeCustomizerElement)
15 |
16 |
17 | const themePageNavLink = document.querySelector("a[href^='#theming']")
18 | const aboutPageElement = document.querySelector("#page-about")
19 | const aboutPageNavLink = document.querySelector("a[href^='#about']")
20 |
21 | //Tab routing for options page
22 | router.get('theming', function(request) {
23 | const otherPages = Array.from(document.querySelectorAll("div[id^='page-']")).filter((el) => el !== appearancePageElement)
24 | otherPages.forEach((page) => page.classList.add('hide'))
25 |
26 | const otherNavLinks = Array.from(document.querySelectorAll("a[href^='#']")).filter((el) => el != themePageNavLink)
27 | otherNavLinks.forEach((navLink) => navLink.classList.remove('is-active'))
28 | themePageNavLink.classList.add('is-active')
29 |
30 | appearancePageElement.classList.remove('hide')
31 | })
32 |
33 |
34 | router.get('about', function (request) {
35 | const otherPages = Array.from(document.querySelectorAll("div[id^='page-']")).filter((el) => el !== aboutPageElement)
36 | otherPages.forEach((page) => page.classList.add('hide'))
37 |
38 | const otherNavLinks = Array.from(document.querySelectorAll("a[href^='#']")).filter((el) => el != aboutPageNavLink)
39 | otherNavLinks.forEach((navLink) => navLink.classList.remove('is-active'))
40 | aboutPageNavLink.classList.add('is-active')
41 |
42 | aboutPageElement.classList.remove('hide')
43 | })
44 |
45 | /**
46 | * Google Analytics
47 | * Purpose in Cato app:
48 | * - Used as a counter for tracking cato command usage. This does not track what you write inside the application.
49 | * - For this particular page, I want to see if people are actually using the theme customizer.
50 |
51 | * - As the app developer of this app, who is building this app in his spare time at no cost to users,
52 | * I'd like to know how useful this app is to users. Measuring how many times the app is launched gives me an idea how often this
53 | * application is being used.
54 |
55 | */
56 |
57 | // // Standard Google Universal Analytics code
58 | (function (i, s, o, g, r, a, m) {
59 | i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
60 | (i[r].q = i[r].q || []).push(arguments)
61 | }, i[r].l = 1 * new Date(); a = s.createElement(o),
62 | m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
63 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); // Note: https protocol here
64 |
65 | ga('create', 'UA-41444051-3', 'auto'); // Enter your GA identifier
66 | ga('set', 'checkProtocolTask', function () { }); // Removes failing protocol check. @see: http://stackoverflow.com/a/22152353/1958200
67 | ga('require', 'displayfeatures');
68 | ga('send', 'pageview', '/options.html'); // Specify the virtual path
69 |
--------------------------------------------------------------------------------
/app/pages/options/options.scss:
--------------------------------------------------------------------------------
1 | @import "../../../node_modules/normalize.css/normalize.css";
2 | @import "../../../node_modules/bulma/css/bulma.css";
3 | @import "../../components/command-palette/command-palette.scss";
4 | @import "../../components/static-command-palette/static-command-palette.scss";
5 | @import "../../components/theme-customizer/theme-customizer.scss";
6 |
7 | body {
8 | margin-left: 200px;
9 | }
10 |
11 | .hide {
12 | display: none !important;
13 | }
14 |
15 | .sidebar {
16 | background: #222222;
17 | width: 180px;
18 | padding: 30px 0;
19 | z-index: 9999;
20 | position: fixed;
21 | top: 0;
22 | left: 0;
23 | bottom: 0;
24 |
25 | &__title {
26 | font-size: 30px;
27 | color: white;
28 | padding-left: 16px;
29 | }
30 |
31 | &__nav {
32 | margin-top: 30px;
33 | display: flex;
34 | flex-direction: column;
35 |
36 | &-link {
37 | text-decoration: none;
38 | color: white !important;
39 | padding: 16px;
40 | font-size: 16px;
41 |
42 | &.is-active,
43 | &:hover {
44 | border-left: 4px solid white;
45 | }
46 |
47 | img {
48 | height: 20px;
49 | margin-right: 10px;
50 | vertical-align: bottom;
51 | }
52 | }
53 | }
54 |
55 | .sidebar__footer {
56 | position: absolute;
57 | bottom: 15px;
58 | left: 16px;
59 | right: 16px;
60 |
61 | &-links {
62 | margin: 0 10px 0 0;
63 |
64 | img {
65 | height: 20px;
66 | }
67 | }
68 | }
69 | }
70 |
71 | .main-content {
72 | margin-top: 25px;
73 | margin-left: 80px;
74 | margin-right: 80px;
75 | min-width: 500px;
76 | }
77 |
78 | #page-theme {
79 | display: flex;
80 | }
81 |
82 | #page-about {
83 | .cLauncher-header {
84 | background: #222222;
85 |
86 | &__banner {
87 | max-height: 200px;
88 | display: block;
89 | margin: 0 auto;
90 | }
91 | }
92 | }
93 |
94 | #reset-theme {
95 | background: hotpink;
96 | color: white;
97 | }
98 |
99 | .author-message-container {
100 | margin-top: 60px;
101 | max-width: 400px;
102 |
103 | .author-avatar {
104 | border-radius: 50%;
105 | margin-right: 10px;
106 | }
107 |
108 | .author-message {
109 | padding-top: 10px;
110 | margin-left: 10px;
111 | font-size: 14px;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/app/pages/popup/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | App
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/pages/popup/popup.js:
--------------------------------------------------------------------------------
1 | require('../popup/popup.scss')
2 | const utils = require('../../util.js')
3 | const browser = require('webextension-polyfill')
4 | const defaultSuggestions = require('../../plugins/plugins.js')
5 | const appHTML = require("../../components/command-palette/command-palette.html")
6 | const fallbackWebSearches = require( '../../plugins/fallback-web-searches/index.js')
7 |
8 | /**
9 | * @description
10 | * 1. Creates app-used counter in localstorage if undefined, else use existing
11 | * 2. Creates themeConfig object in localstorage if undefined, else use existing
12 | */
13 | async function setupLocalStorage() {
14 | const {themeConfig} = await browser.storage.sync.get('themeConfig')
15 | if(themeConfig === undefined) {
16 | await browser.storage.sync.set({themeConfig: utils.defaultThemeConfig})
17 | }
18 | // await browser.storage.sync.clear()
19 |
20 | }
21 | setupLocalStorage();
22 |
23 |
24 | /**
25 | * @description
26 | * Sets the colors for our app (inside popup window) from our defaultThemeConfig stored in localStorage.
27 | * We iterate over each key in our defaultThemeConfig, which corresponds to a
28 | * global CSS variable in our app, and assign the value from our defaultThemeConfig
29 | * to our global CSS variables.
30 | * @returns {Promise}
31 | */
32 | async function setupAppTheme() {
33 | //we have css variables defined in our css already. Now we just modify them if we have values in our config
34 | const {themeConfig} = await browser.storage.sync.get('themeConfig')
35 |
36 | for (const key in themeConfig) {
37 | if (themeConfig.hasOwnProperty(key)) {
38 | utils.updateCSSVariable(key, themeConfig[key])
39 | }
40 | }
41 | }
42 | setupAppTheme()
43 |
44 | /**
45 | * @description
46 | * Saves the search scopes for some our plugins. For some of our actions like 'Find Bookmark',
47 | * we prepend the search scope for the action to our input and allow the user
48 | * to type his/her query after the search scope name. Ex 'Find Bookmark 101 ways to cook'.
49 | *
50 | * ['Find Bookmark ', 'Change Tab']
51 | */
52 | const searchScopes = []
53 |
54 | defaultSuggestions.forEach((plugin) => {
55 | if(plugin.searchScope !== undefined) {
56 | searchScopes.push(plugin.searchScope.toLowerCase())
57 | }
58 | })
59 |
60 | /* ***********************Bootstrap app markup & set up globals**************************/
61 | window.appElement = document.createElement("div")
62 | window.appElement.innerHTML = appHTML
63 | document.body.appendChild(window.appElement)
64 | window.defaultSuggestions = [...defaultSuggestions];
65 | window.currentSearchSuggestions = [...defaultSuggestions]
66 | window.searchInput = document.querySelector(".cLauncher__search")
67 | window.userQuery = ''
68 | window.searchResultsList = document.querySelector(".cLauncher__suggestions")
69 |
70 | function rerenderSuggestions(event) {
71 | window.isWebSearch = false;
72 | window.isCalculatorResult = false;
73 | window.searchResultsList.innerHTML = ""
74 | if(window.searchInput.value === '') {
75 | window.currentSearchSuggestions = [...defaultSuggestions]
76 | window.searchResultsList.innerHTML = ""
77 | }
78 |
79 |
80 | let domain = searchScopes.filter((searchScope) => {
81 | if (window.searchInput.value.toLowerCase().includes(searchScope)) {
82 | return searchScope
83 | }
84 | })[0]
85 |
86 |
87 |
88 | //if we have "Find Bookmark apple" in the search input only pass everything after for searching.
89 | if (domain) {
90 | //userQuery = 'find bookmark async'
91 | //trimedQuery for matches = 'async'
92 |
93 | //find the plugin to execute based off the domain.
94 | let x = defaultSuggestions.filter((plugin) => {
95 | let regex = new RegExp(domain, 'i')
96 | if(plugin.searchScope != undefined){
97 | return plugin.searchScope.match(regex)
98 | }
99 | })
100 |
101 | let regex = new RegExp(`${domain } `, 'i')
102 | let query = window.searchInput.value.split(regex).filter((x) => x.length !== 0).join()
103 | window.userQuery = query
104 |
105 | const matches = utils.getMatches(query, window.currentSearchSuggestions)
106 | utils.renderSuggestions(matches)
107 | }
108 |
109 | else {
110 | let matches = utils.getMatches(window.searchInput.value, window.currentSearchSuggestions)
111 |
112 | if(matches.length > 0) {
113 | utils.renderSuggestions(matches)
114 | }
115 |
116 | else if(utils.displayPotentialMathResult(window.searchInput.value).length > 0){
117 | utils.renderSuggestions(utils.displayPotentialMathResult(window.searchInput.value))
118 | window.isCalculatorResult = true;
119 | ga('send', 'event', 'calculatorResultShowedUp', 'true')
120 | }
121 |
122 | else if (matches.length === 0 && window.searchInput.value !== '') {
123 | const results = fallbackWebSearches.map((webSearch) => webSearch(window.searchInput.value))
124 | utils.renderSuggestions(results)
125 | }
126 | }
127 | window.suggestionElements = document.querySelectorAll('.cLauncher__suggestion')
128 | }
129 | window.searchInput.addEventListener("input", rerenderSuggestions)
130 |
131 |
132 | /**
133 | * Handles up and down keys when the user has focus on the input field, enabling the user
134 | * to select the search.
135 | * @param {event} event
136 | * @returns {void}
137 | */
138 | function handleArrowKeysOnInput(event) {
139 | window.searchInput.focus()
140 | window.selectedElement = document.querySelector('.selected')
141 |
142 | if(window.selectedElement) {
143 | const key = {DOWN: event.keyCode === 40, TAB: event.keyCode === 9, UP: event.keyCode === 38, ENTER: event.keyCode === 13}
144 | if(!window.selectedElement) return;
145 |
146 | if(key.DOWN || key.TAB) {
147 | // prevent focus on search input so we can keep tabbing down our suggestions while still being able to type
148 | event.preventDefault()
149 | if (selectedElement.nextElementSibling !== null) {
150 | selectedElement.classList.remove('selected')
151 | selectedElement = selectedElement.nextElementSibling
152 | selectedElement.classList.add('selected')
153 | selectedElement.scrollIntoView(false)
154 | } else {
155 | //we've reached the end of the list, select the top element
156 | selectedElement.classList.remove('selected')
157 | selectedElement = window.suggestionElements[0]
158 | selectedElement.classList.add('selected')
159 | selectedElement.scrollIntoView(true)
160 | }
161 | }
162 | else if(key.UP) {
163 | if (selectedElement.previousElementSibling !== null) {
164 | selectedElement.classList.remove('selected')
165 | selectedElement = selectedElement.previousElementSibling
166 | selectedElement.classList.add('selected')
167 | if(isVisibleY(selectedElement) === false) {
168 | selectedElement.scrollIntoView(true)
169 | }
170 | } else {
171 | //we've reached the top of the list
172 | selectedElement.classList.remove('selected')
173 | selectedElement = window.suggestionElements[window.suggestionElements.length - 1]
174 | selectedElement.classList.add('selected')
175 | selectedElement.scrollIntoView(false)
176 | }
177 | }
178 | else if(key.ENTER && window.searchInput.value) {
179 | window.selectedElement.click();
180 | const searchInputValue = window.searchInput.value.toLowerCase();
181 | const catoCommandName = window.selectedElement.querySelector('.cLauncher__suggestion-title').innerText.toLowerCase(); //This is not the value inside of the search input box.
182 | const catoCommandSubtitle = window.selectedElement.querySelector('.cLauncher__suggestion-subtitle').innerText.toLowerCase();
183 | const isFuzzySearchExecution = catoCommandName === searchInputValue ? 'false' : 'true'; // testing how often a full command is typed versus using fuzzy search.
184 | const isWebSearch = /^(http|https|www|search|for)/.test(catoCommandName) || /^(http|https|www|search|for)/.test(catoCommandSubtitle);
185 | const isCalculatorResult = catoCommandSubtitle.includes("copy number to");
186 | //https://developers.google.com/analytics/devguides/collection/analyticsjs/events
187 |
188 | if(!isWebSearch && !isCalculatorResult) {
189 | ga('send', 'event', 'executeCommand', catoCommandName);
190 | ga('send', 'event', 'fuzzySearch', isFuzzySearchExecution);
191 | }
192 |
193 | if(isWebSearch) {
194 | ga('send', 'event', 'webSearchCommand', 'true'); // how often do people use web searches? To readers, we don't track queries, just whether you ran a websearch command or not.
195 | }
196 |
197 | if(window.isCalculatorResult) {
198 | ga('send', 'event', 'calculatorCopyCommand', 'true'); // how often do people use the calculator? I have a hypothesis that people use the calculator a lot for quick calculations but don't copy to clipboard
199 | }
200 |
201 | window.suggestionElements.forEach((result, index) => {
202 | if (result.classList.contains('selected')) {
203 | ga('send', 'event', 'hitPosition', index+1);
204 | }
205 | })
206 | }
207 | /**
208 | * Check if an element is visible inside of it's parent container
209 | * @param {HTMLelement} element
210 | * @return {Boolean}
211 | */
212 | function isVisibleY(element) {
213 | const elementRect = element.getBoundingClientRect()
214 | const parentElementRect = element.parentElement.getBoundingClientRect()
215 | if(elementRect.top <= parentElementRect.bottom === false) return false;
216 | //check if the element is out of view due to a container scrolling.
217 | if( (elementRect.top + elementRect.height) <= parentElementRect.top ) return false;
218 | return elementRect.top <= document.documentElement.clientHeight
219 | }
220 | }
221 | }
222 |
223 | window.searchInput.addEventListener("keydown", handleArrowKeysOnInput)
224 |
225 |
226 | browser.commands.onCommand.addListener(function(command) {
227 | if (command == "toggle-feature") {
228 | console.log("toggling the feature!");
229 | setTimeout(() => {
230 | document.focus()
231 | window.searchInput.focus()
232 | },1000)
233 | }
234 | });
235 |
236 | /**
237 | * Google Analytics
238 | * Purpose in Cato app:
239 | * - Used as a counter for tracking cato command usage. This does not track what you write inside the application.
240 | *
241 | * - As the app developer of this app, who is building this app in his spare time at no cost to users,
242 | * I'd like to know how useful this app is to users. Measuring how many times the app is launched gives me an idea how often this
243 | * application is being used.
244 | */
245 |
246 | // // Standard Google Universal Analytics code
247 | (function (i, s, o, g, r, a, m) {
248 | i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
249 | (i[r].q = i[r].q || []).push(arguments)
250 | }, i[r].l = 1 * new Date(); a = s.createElement(o),
251 | m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
252 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); // Note: https protocol here
253 |
254 | ga('create', 'UA-41444051-3', 'auto'); // Enter your GA identifier
255 | ga('set', 'checkProtocolTask', function () { }); // Removes failing protocol check. @see: http://stackoverflow.com/a/22152353/1958200
256 | ga('require', 'displayfeatures');
257 | ga('send', 'pageview', '/popup.html'); // Specify the virtual path
258 |
--------------------------------------------------------------------------------
/app/pages/popup/popup.scss:
--------------------------------------------------------------------------------
1 | @import "../../../node_modules/normalize.css/normalize.css";
2 | @import "../../components/command-palette/command-palette.scss";
3 |
4 | //START: Border-sizing reset
5 | html {
6 | box-sizing: border-box;
7 | }
8 | *, *:before, *:after {
9 | box-sizing: inherit;
10 | }
11 | //END: Border-sizing reset
12 |
13 |
14 | :root {
15 | --app-width-size: 500px;
16 | }
17 | html {
18 | //This needs to be done on the popup. Not the command pallete
19 | //or else when we search & get for example 1 result, we'll see the full
20 | //height of the popupstill.
21 | min-height: 60px;
22 | width: var(--app-width-size);
23 | }
24 |
25 | body {
26 | margin: 0;
27 | padding: 0;
28 | }
29 |
--------------------------------------------------------------------------------
/app/plugins/close-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Close Tab",
4 | subtitle: 'Close the current tab.',
5 | action: closeTab,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function closeTab() {
12 | const activeTab = await browser.tabs.query({active: true, currentWindow: true})
13 | await browser.tabs.remove(activeTab[0].id)
14 | const popupWindow = await browser.extension.getViews({type: 'popup'})[0]
15 | popupWindow.close()
16 | }
17 |
18 | module.exports = plugin
19 |
--------------------------------------------------------------------------------
/app/plugins/close-tabs-except-active/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Close Tabs Except Current",
4 | subtitle: 'Closes all tabs in this window except this active one.',
5 | action: closeTabsExceptCurrent,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function closeTabsExceptCurrent() {
12 | const otherTabs = await browser.tabs.query({'active': false, currentWindow: true})
13 | for(const tab of otherTabs) {
14 | await browser.tabs.remove(tab.id)
15 | }
16 | const popupWindow = await browser.extension.getViews({type: 'popup'})[0]
17 | popupWindow.close()
18 | }
19 |
20 | module.exports = plugin
21 |
--------------------------------------------------------------------------------
/app/plugins/copy-url/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const { copyToClipboard } = require('../../util');
3 | const plugin = {
4 | keyword: "Copy URL",
5 | subtitle: 'Copy the URL of the current page.',
6 | action: copyUrl,
7 | icon: {
8 | path: 'images/chrome-icon.svg'
9 | }
10 | }
11 |
12 | async function copyUrl() {
13 | const tabs = await browser.tabs.query({active: true, currentWindow: true})
14 | const activeTabUrl = tabs[0].url
15 |
16 | copyToClipboard(activeTabUrl, ev => {
17 | window.close();
18 | });
19 | }
20 |
21 | module.exports = plugin
22 |
--------------------------------------------------------------------------------
/app/plugins/detach-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Detach Tab",
4 | subtitle: 'Detach the current tab & place it in new window.',
5 | action: detachTab,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function detachTab() {
12 | const tabs = await browser.tabs.query({active: true, currentWindow: true})
13 | const currentTab = tabs[0]
14 | const newWindow = await browser.windows.create({tabId: currentTab.id})
15 | await browser.tabs.move({windowId: newWindow.id, index: -1})
16 | }
17 |
18 | module.exports = plugin
19 |
--------------------------------------------------------------------------------
/app/plugins/disable-extension/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const utils = require('../../util.js')
3 | const sortBy = require('lodash.sortby')
4 |
5 | const plugin = {
6 | keyword: 'Disable Extension',
7 | subtitle: 'Disable an active chrome extension',
8 | valid: false,
9 | searchScope: 'Disable Extension',
10 | action: displayActiveExtensions,
11 | icon: {
12 | path: 'images/chrome-icon.svg'
13 | }
14 | }
15 |
16 |
17 |
18 | async function displayActiveExtensions() {
19 | window.currentSearchSuggestions = []
20 |
21 | if (!plugin.valid) {
22 | window.searchInput.value = `${plugin.searchScope} `
23 | window.searchResultsList.innerHTML = ""
24 |
25 | const extensionsAndApps = await browser.management.getAll()
26 | let extensions = extensionsAndApps.filter((extensionOrApp) => {
27 | return extensionOrApp.type === 'extension' &&
28 | extensionOrApp.name !== "Cato" &&
29 | extensionOrApp.enabled
30 | })
31 |
32 | extensions = sortBy(extensions, ['name'])
33 |
34 | window.currentSearchSuggestions = extensions.map((extension) => {
35 | const action = {
36 | keyword: `${extension.name}`,
37 | action: async () => {
38 | await browser.management.setEnabled(extension.id, false)
39 | window.close()
40 | },
41 | icon: {
42 | path: utils.useAvalableExtensionIcon(extension)
43 | }
44 | }
45 | return action
46 | })
47 |
48 | // display a message if nothing found
49 | if (window.currentSearchSuggestions.length === 0) {
50 | const noInactiveExtensionMessage = {
51 | keyword: 'No Other Active Extensions Found.',
52 | icon: {
53 | path: 'images/chrome-icon.svg'
54 | }
55 | }
56 | window.currentSearchSuggestions.push(noInactiveExtensionMessage)
57 | utils.renderSuggestions(window.currentSearchSuggestions)
58 | }
59 | else {
60 | utils.renderSuggestions(window.currentSearchSuggestions)
61 | window.suggestionElements = document.querySelectorAll('.cLauncher__suggestion')
62 | }
63 |
64 | }
65 | }
66 |
67 | module.exports = plugin
68 |
--------------------------------------------------------------------------------
/app/plugins/disable-extensions/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Disable All Extensions",
4 | subtitle: 'Disables all active chrome extensions',
5 | action: disableAllExtensions,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function disableAllExtensions() {
12 | const extensionsAndApps = await browser.management.getAll()
13 | const extensions = extensionsAndApps
14 | .filter((extensionOrApp) => {
15 | return extensionOrApp.type === 'extension' &&
16 | extensionOrApp.name !== "Cato"
17 | })
18 | extensions.forEach((extension) => browser.management.setEnabled(extension.id, false))
19 | window.close()
20 | }
21 |
22 | module.exports = plugin
23 |
--------------------------------------------------------------------------------
/app/plugins/duplicate-tab-incognito/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Duplicate Tab Incognito",
4 | subtitle: 'Duplicate the tab and place it in an incognito window.',
5 | action: duplicateTabIncognito,
6 | icon: {
7 | path: 'images/incognito-icon.svg'
8 | }
9 | }
10 |
11 | async function duplicateTabIncognito() {
12 | const allTabs = await browser.tabs.query({active: true, currentWindow: true})
13 | const activeTab = allTabs[0]
14 | const activeTabUrl = allTabs[0].url
15 | await browser.windows.create({incognito: true, url: activeTabUrl})
16 | }
17 |
18 | module.exports = plugin
19 |
--------------------------------------------------------------------------------
/app/plugins/duplicate-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 |
3 | const plugin = {
4 | keyword: "Duplicate Tab",
5 | subtitle: 'Duplicate the tab and place it in a new tab.',
6 | action: duplicateTab,
7 | icon: {
8 | path: 'images/chrome-icon.svg'
9 | }
10 | }
11 |
12 | async function duplicateTab() {
13 | const allTabs = await browser.tabs.query({active: true, currentWindow: true})
14 | const activeTabUrl = allTabs[0].url
15 | await browser.tabs.create({url: activeTabUrl})
16 | }
17 |
18 | module.exports = plugin
19 |
--------------------------------------------------------------------------------
/app/plugins/enable-extension/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Extension Shortcuts",
4 | subtitle: 'View or setup shortcuts for your chrome extensions',
5 | action: openExtensionShortcuts,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openExtensionShortcuts() {
12 | await browser.tabs.create({ url: "chrome://extensions/shortcuts" })
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/fallback-web-searches/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const fallbackWebSearches = [
3 |
4 | //google search
5 | function fallbackSearch(query) {
6 |
7 | function search(query) {
8 | return async function closureFunc() {
9 | await browser.tabs.create({ url: `https://www.google.com/search?q=${encodeURIComponent(query)}` })
10 | }
11 | }
12 |
13 | return {
14 | keyword: `Search Google for: '${query}'`,
15 | action: search(query),
16 | icon: {
17 | path: 'images/google-search-icon.svg'
18 | }
19 | }
20 | },
21 |
22 | //YouTube
23 | function fallbackSearch(query) {
24 |
25 | function search(query) {
26 | return async function closureFunc() {
27 | await browser.tabs.create({ url: `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}` })
28 | }
29 | }
30 |
31 | return {
32 | keyword: `Search Youtube for: '${query}'`,
33 | action: search(query),
34 | icon: {
35 | path: 'images/youtube-icon.svg'
36 | }
37 | }
38 | },
39 |
40 | //google maps
41 | // function fallbackSearch(query) {
42 | //
43 | // function search(query) {
44 | // return async function closureFunc() {
45 | // await browser.tabs.create({ url: `https://www.google.com/maps?q=${encodeURIComponent(query)}` })
46 | // }
47 | // }
48 | //
49 | // return {
50 | // keyword: `Search Google Maps for: '${query}'`,
51 | // action: search(query),
52 | // icon: {
53 | // path: 'images/google-maps-icon.svg'
54 | // }
55 | // }
56 | // },
57 |
58 | //google translate
59 | // function fallbackSearch(query) {
60 | //
61 | // function search(query) {
62 | // return async function closureFunc() {
63 | // await browser.tabs.create({ url: `https://translate.google.com/?text=${encodeURIComponent(query)}` })
64 | // }
65 | // }
66 | //
67 | // return {
68 | // keyword: `Search Google Translate for: '${query}'`,
69 | // action: search(query),
70 | // icon: {
71 | // path: 'images/google-translate-icon.png'
72 | // }
73 | // }
74 | // },
75 |
76 | //google drive
77 | function fallbackSearch(query) {
78 |
79 | function search(query) {
80 | return async function closureFunc() {
81 | await browser.tabs.create({ url: `https://drive.google.com/drive/u/0/search?q=${encodeURIComponent(query)}` })
82 | }
83 | }
84 |
85 | return {
86 | keyword: `Search Google Drive for: '${query}'`,
87 | action: search(query),
88 | icon: {
89 | path: 'images/google-drive-icon.svg'
90 | }
91 | }
92 | },
93 |
94 | //gmail
95 | function fallbackSearch(query) {
96 |
97 | function search(query) {
98 | return async function closureFunc() {
99 | await browser.tabs.create({ url: `https://mail.google.com/mail/u/0/#search/${encodeURIComponent(query)}` })
100 | }
101 | }
102 |
103 | return {
104 | keyword: `Search Gmail for: '${query}'`,
105 | action: search(query),
106 | icon: {
107 | path: 'images/gmail-icon.svg'
108 | }
109 | }
110 | },
111 |
112 | //amazon
113 | // function fallbackSearch(query) {
114 | //
115 | // function search(query) {
116 | // return async function closureFunc() {
117 | // await browser.tabs.create({ url: `https://www.amazon.com/s/?url=search-alias%3Daps&field-keywords=${encodeURIComponent(query)}` })
118 | // }
119 | // }
120 | //
121 | // return {
122 | // keyword: `Search Amazon for: '${query}'`,
123 | // action: search(query),
124 | // icon: {
125 | // path: 'images/amazon-icon.svg'
126 | // }
127 | // }
128 | // },
129 |
130 |
131 | //WikiPedia
132 | // function fallbackSearch(query) {
133 | //
134 | // function search(query) {
135 | // return async function closureFunc() {
136 | // await browser.tabs.create({ url: `http://en.wikipedia.org/wiki/${encodeURI(query)}` })
137 | // }
138 | // }
139 | //
140 | // return {
141 | // keyword: `Search Wikipedia for: '${query}'`,
142 | // action: search(query),
143 | // icon: {
144 | // path: 'images/wikipedia-icon.png'
145 | // }
146 | // }
147 | // },
148 |
149 | //Github
150 | // function fallbackSearch(query) {
151 | //
152 | // function search(query) {
153 | // return async function closureFunc() {
154 | // await browser.tabs.create({ url: `https://github.com/search?utf8=%E2%9C%93&q=${encodeURI(query)}` })
155 | // }
156 | // }
157 | //
158 | // return {
159 | // keyword: `Search Github for: '${query}'`,
160 | // action: search(query),
161 | // icon: {
162 | // path: 'images/wikipedia-icon.png'
163 | // }
164 | // }
165 | // },
166 |
167 | //Twitter
168 | // function fallbackSearch(query) {
169 | //
170 | // function search(query) {
171 | // return async function closureFunc() {
172 | // await browser.tabs.create({ url: `https://twitter.com/search?q=${encodeURI(query)}` })
173 | // }
174 | // }
175 | //
176 | // return {
177 | // keyword: `Search Twitter for: '${query}'`,
178 | // action: search(query),
179 | // icon: {
180 | // path: 'images/twitter-icon.png'
181 | // }
182 | // }
183 | // }
184 | ]
185 |
186 | module.exports = fallbackWebSearches
187 |
--------------------------------------------------------------------------------
/app/plugins/find-bookmark/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const utils = require('../../util.js')
3 |
4 | const plugin = {
5 | keyword: "Find Bookmark",
6 | subtitle: 'Search for a bookmark and open it.',
7 | valid: false,
8 | searchScope: 'Find Bookmark',
9 | action: findBookmark,
10 | icon: {
11 | path: 'images/chrome-icon.svg'
12 | }
13 | }
14 |
15 | async function findBookmark() {
16 | window.currentSearchSuggestions = []
17 | if(!plugin.valid) {
18 | window.searchInput.value = `${plugin.searchScope} `
19 | window.searchResultsList.innerHTML = ""
20 |
21 | const nodes = await browser.bookmarks.getTree()
22 | recurseTree(nodes)
23 | utils.renderSuggestions(window.currentSearchSuggestions)
24 | window.suggestionElements = document.querySelectorAll('.cLauncher__suggestion')
25 | }
26 | }
27 |
28 | function recurseTree(nodes) {
29 | for (const node of nodes) {
30 | if (isBookmark(node)) {
31 | const suggestion = {
32 | keyword: node.title,
33 | subtitle: node.url,
34 | action: async function () {
35 | await browser.tabs.create({url: node.url})
36 | },
37 | icon: {
38 | path: `chrome://favicon/${node.url}` || 'images/blank-page-icon.svg'
39 | }
40 | }
41 | window.currentSearchSuggestions.push(suggestion)
42 | }
43 | else if (isFolder(node)) {
44 | recurseTree(node.children)
45 | }
46 | }
47 | }
48 |
49 | const isFolder = (node) => 'children' in node
50 | const isBookmark = (node) => 'url' in node
51 | module.exports = plugin
52 |
--------------------------------------------------------------------------------
/app/plugins/find-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const utils = require('../../util.js')
3 |
4 | const plugin = {
5 | keyword: "Change Tab",
6 | subtitle: 'Find an open tab and change to it.',
7 | valid: false,
8 | searchScope: 'Change Tab',
9 | action: findTab,
10 | icon: {
11 | path: 'images/chrome-icon.svg'
12 | }
13 | }
14 |
15 | async function findTab() {
16 | window.currentSearchSuggestions = []
17 | let allTabs = []
18 |
19 | if (!plugin.valid) {
20 | window.searchInput.value = `${plugin.searchScope} `
21 | window.searchResultsList.innerHTML = ""
22 |
23 | const allWindows = await browser.windows.getAll({populate: true})
24 | allWindows.forEach((browserWindow) => {
25 | allTabs = allTabs.concat(browserWindow.tabs)
26 | })
27 |
28 | allTabs.forEach((tab) => {
29 | const suggestion = {
30 | 'action': switchToTabById(tab.windowId, tab.id),
31 | 'icon': {
32 | path: tab.favIconUrl || 'images/blank-page-icon.svg'
33 | },
34 | 'keyword': `${tab.title}`,
35 | subtitle: `${tab.url}`
36 | }
37 | window.currentSearchSuggestions.push(suggestion)
38 | })
39 |
40 | utils.renderSuggestions(window.currentSearchSuggestions)
41 | window.suggestionElements = document.querySelectorAll('.cLauncher__suggestion')
42 | }
43 | }
44 |
45 | //Helper
46 | function switchToTabById(windowId, tabId) {
47 | // tabs.update is limited to switching to tabs only within the current window, thus switch to the window we need first.
48 | return async function closureFunc() {
49 | await browser.windows.update(windowId, {focused:true})
50 | await browser.tabs.update(tabId, {'active': true})
51 | }
52 | }
53 |
54 | module.exports = plugin
55 |
--------------------------------------------------------------------------------
/app/plugins/merge-sort-tabs/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const sortBy = require('lodash.sortby')
3 | const domain = require('getdomain')
4 |
5 | const plugin = {
6 | keyword: "Sort Tabs and Merge Windows",
7 | subtitle: 'Move all tabs to the current window and sort them.',
8 | action: sortAndMergeWindows,
9 | icon: {
10 | path: 'images/chrome-icon.svg'
11 | }
12 | }
13 |
14 | async function sortAndMergeWindows() {
15 | const currentWindow = await browser.windows.getCurrent()
16 | const allWindows = await browser.windows.getAll({populate: true})
17 | let allTabs = []
18 | let index = 0
19 |
20 | allWindows.forEach((browserWindow) => {
21 | browserWindow.tabs.forEach((tab) => {
22 | allTabs.push({id: tab.id, domain: domain.origin(tab.url)})
23 | })
24 |
25 | allTabs = sortBy(allTabs, ['domain'])
26 | allTabs.forEach((tab) => {
27 | browser.tabs.move(tab.id, {windowId: currentWindow.id, index})
28 | index += 1
29 | })
30 | window.close()
31 |
32 | })
33 | }
34 |
35 | module.exports = plugin
36 |
--------------------------------------------------------------------------------
/app/plugins/move-tab-left/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Move Tab Left",
4 | subtitle: 'Swap places with the tab on the right.',
5 | action: moveTabLeft,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function moveTabLeft() {
12 | const allTabs = await browser.tabs.query({active: true, currentWindow: true})
13 | const currentTab = allTabs[0]
14 |
15 | if (currentTab.index !== 0) {
16 | browser.tabs.move(currentTab.id, {index: currentTab.index - 1})
17 | window.close()
18 | }
19 | }
20 |
21 | module.exports = plugin
22 |
--------------------------------------------------------------------------------
/app/plugins/move-tab-right/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Move Tab Right",
4 | subtitle: 'Swap places with the tab on the right.',
5 | action: moveTabRight,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function moveTabRight() {
12 | const allTabs = await browser.tabs.query({'active': true, 'currentWindow': true})
13 | const currentTab = allTabs[0]
14 | browser.tabs.move(currentTab.id, {index: currentTab.index + 1})
15 | window.close()
16 | }
17 |
18 | module.exports = plugin
19 |
--------------------------------------------------------------------------------
/app/plugins/next-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const utils = require('../../util.js')
3 |
4 | const plugin = {
5 | keyword: "Next Tab",
6 | subtitle: 'Change to the next tab in your browser window.',
7 | action: nextTab,
8 | icon: {
9 | path: 'images/chrome-icon.svg'
10 | }
11 | }
12 |
13 | async function nextTab() {
14 | const allTabs = await browser.tabs.query({currentWindow: true})
15 | let activeTabIndex = allTabs.findIndex((tab) => tab.active)
16 | let nextTabIndex = activeTabIndex + 1 === allTabs.length ? 0 : activeTabIndex + 1
17 | const nextTab = allTabs[nextTabIndex]
18 | await browser.tabs.update(nextTab.id , {active: true})
19 | }
20 |
21 | module.exports = plugin
22 |
--------------------------------------------------------------------------------
/app/plugins/open-about/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "About",
4 | subtitle: 'View general information about your browser.',
5 | action: openSettings,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openSettings() {
12 | await browser.tabs.create({
13 | url: "chrome://settings/help"
14 | });
15 | }
16 |
17 | module.exports = plugin;
18 |
--------------------------------------------------------------------------------
/app/plugins/open-amazon/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 |
3 | const plugin = {
4 | keyword: "Amazon",
5 | subtitle: 'Open Amazon.',
6 | action: openAmazon,
7 | icon: {
8 | path: 'images/amazon-icon.svg'
9 | }
10 | }
11 |
12 | async function openAmazon() {
13 | await browser.tabs.create({url: "https://amazon.com"})
14 | }
15 |
16 | module.exports = plugin
17 |
--------------------------------------------------------------------------------
/app/plugins/open-app-settings/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Cato Settings",
4 | subtitle: 'View the settings page for Cato (this app).',
5 | action: openApplicationOptions,
6 | icon: {
7 | path: 'images/cato-logo.png'
8 | }
9 | }
10 |
11 | async function openApplicationOptions() {
12 | await browser.runtime.openOptionsPage()
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-apps/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 |
3 | const plugin = {
4 | keyword: "Apps",
5 | subtitle: 'View your installed browser applications.',
6 | action: openApps,
7 | icon: {
8 | path: 'images/chrome-icon.svg'
9 | }
10 | }
11 |
12 | async function openApps() {
13 | await browser.tabs.create({url: "chrome://apps"})
14 | }
15 |
16 | module.exports = plugin
17 |
--------------------------------------------------------------------------------
/app/plugins/open-bookmarks/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Bookmarks",
4 | subtitle: 'View your bookmarks page.',
5 | action: openBookmarks,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openBookmarks() {
12 | await browser.tabs.create({url: "chrome://bookmarks"})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-downloads/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Downloads",
4 | subtitle: 'View your browser downloads.',
5 | action: openDownloads,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openDownloads() {
12 | browser.tabs.create({url: "chrome://downloads"})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-extensions/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Extensions",
4 | subtitle: 'View your installed browser extensions.',
5 | action: openExtensions,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openExtensions() {
12 | await browser.tabs.create({url: "chrome://extensions"})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-gmail/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Gmail",
4 | subtitle: 'Open Gmail',
5 | action: openGmail,
6 | icon: {
7 | path: 'images/gmail-icon.svg'
8 | }
9 | }
10 |
11 | async function openGmail() {
12 | await browser.tabs.create({url: "https://mail.google.com"})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-google-calendar/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Google Calendar",
4 | subtitle: 'Open Google Calendar',
5 | action: openGoogleCalendar,
6 | icon: {
7 | path: 'images/google-calendar-icon.svg'
8 | }
9 | }
10 |
11 | async function openGoogleCalendar() {
12 | await browser.tabs.create({url: "https://calendar.google.com"});
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-google-drive/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Google Drive",
4 | subtitle: 'Open Google Drive.',
5 | action: openGoogleDrive,
6 | icon: {
7 | path: 'images/google-drive-icon.svg.svg'
8 | }
9 | }
10 |
11 | async function openGoogleDrive() {
12 | await browser.tabs.create({url: "https://drive.google.com"})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-history/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "History",
4 | subtitle: 'Open your search history.',
5 | action: openHistory,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openHistory() {
12 | browser.tabs.create({url: "chrome://history"})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-incognito/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Incognito Window",
4 | subtitle: 'Open a new incognito browser window.',
5 | action: openIncognitoWindow,
6 | icon: {
7 | path: 'images/incognito-icon.svg'
8 | }
9 | }
10 |
11 | async function openIncognitoWindow() {
12 | await browser.windows.create({'incognito': true})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-new-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "New Tab",
4 | subtitle: 'Open a new tab in the current window.',
5 | action: openNewTab,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openNewTab() {
12 | await browser.tabs.create({})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-settings/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Settings",
4 | subtitle: 'Open the browser\'s settings page.',
5 | action: openSettings,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openSettings() {
12 | await browser.tabs.create({url: "chrome://settings"})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/open-window/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "New Window",
4 | subtitle: 'Open a new browser window.',
5 | action: openNewWindow,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function openNewWindow() {
12 | browser.windows.create({})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/page-back/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Back",
4 | subtitle: 'Go back a page.',
5 | action: goPageBack,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function goPageBack() {
12 | await browser.tabs.executeScript(null, {code: `window.history.back()`})
13 | window.close()
14 | }
15 |
16 | module.exports = plugin
17 |
--------------------------------------------------------------------------------
/app/plugins/page-forward/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Forward",
4 | subtitle: 'Go forward a page.',
5 | action: goPageForward,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function goPageForward() {
12 | await browser.tabs.executeScript(null, {code: `window.history.forward()`})
13 | window.close()
14 | }
15 |
16 | module.exports = plugin
17 |
--------------------------------------------------------------------------------
/app/plugins/plugins.js:
--------------------------------------------------------------------------------
1 | const context = require.context('./', true, /index\.js/)
2 |
3 | /**
4 | * Require's the `index.js` file from each plugin directory.
5 | * The result of each require is
6 | * @param {Object} ctx
7 | * @returns {array} An array of objects. Each object is a plugin.
8 | */
9 | const requireAllPlugins = function(ctx) {
10 | const keys = ctx.keys()
11 | //array of values from each index.js require
12 | const values = keys.map(ctx)
13 | return values
14 | }
15 | const allPlugins = requireAllPlugins(context)
16 |
17 | //TODO: change filename back to index.js?, but I need to figure out how
18 | //to tell tell require.context to not include this file or else
19 | //an extra empty object gets created as a plugin.
20 | module.exports = [...allPlugins]
21 |
--------------------------------------------------------------------------------
/app/plugins/previous-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const utils = require('../../util.js')
3 |
4 | const plugin = {
5 | keyword: "Previous Tab",
6 | subtitle: 'Change to the previous tab in your browser window.',
7 | action: nextTab,
8 | icon: {
9 | path: 'images/chrome-icon.svg'
10 | }
11 | }
12 |
13 | async function nextTab() {
14 | const allTabs = await browser.tabs.query({currentWindow: true})
15 | let activeTabIndex = allTabs.findIndex((tab) => tab.active)
16 | let previousTabIndex = (activeTabIndex - 1 < 0) ? allTabs.length - 1 : activeTabIndex - 1
17 | const nextTab = allTabs[previousTabIndex]
18 | await browser.tabs.update(nextTab.id , {active: true})
19 | }
20 |
21 | module.exports = plugin
22 |
--------------------------------------------------------------------------------
/app/plugins/print-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Print Tab",
4 | subtitle: 'Display the print menu for the current page.',
5 | action: printTab,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function printTab() {
12 | await browser.tabs.executeScript(null, {code: `window.print();`})
13 | }
14 |
15 | module.exports = plugin
16 |
--------------------------------------------------------------------------------
/app/plugins/reload-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Reload Tab",
4 | subtitle: 'Reload the current tab.',
5 | action: reloadTab,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function reloadTab() {
12 | await browser.tabs.reload()
13 | window.close()
14 | }
15 |
16 | module.exports = plugin
17 |
--------------------------------------------------------------------------------
/app/plugins/reload-tabs/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Reload All Tabs",
4 | subtitle: 'Reload all tabs in the current window.',
5 | action: reloadAllTabs,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function reloadAllTabs() {
12 | const allTabs = await browser.tabs.query({currentWindow: true})
13 | for (const tab of allTabs) {
14 | await browser.tabs.reload(tab.id)
15 | }
16 | window.close()
17 | }
18 |
19 | module.exports = plugin
20 |
--------------------------------------------------------------------------------
/app/plugins/shorten-url/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill');
2 | const {copyToClipboard, renderSuggestions} = require('../../util');
3 |
4 | const plugin = {
5 | keyword: "Shorten URL",
6 | subtitle: 'Shorten the current url.',
7 | action: shortenUrl,
8 | icon: {
9 | path: 'images/shortened-url-icon.svg'
10 | }
11 | };
12 |
13 | async function shortenUrl() {
14 | const tabs = await browser.tabs.query({active: true,currentWindow: true});
15 |
16 | const activeTabUrl = tabs[0].url;
17 | const postUrl = 'https://www.googleapis.com/urlshortener/v1/url?key=AIzaSyAMSEYpKsnx7pK1I-oKYX7UMC2wumDDjk8'; // Free key allows for 1,000,000 requests per day
18 |
19 | const request = new Request(postUrl, {
20 | method: 'POST',
21 | body: JSON.stringify({longUrl: activeTabUrl}),
22 | mode: 'cors',
23 | headers: new Headers({'Content-Type': 'application/json'})
24 | });
25 |
26 | //clear suggestions display a temporary suggestion while we wait for a response
27 | const temporarySuggestion = {
28 | keyword: "Attempting to shorten url.",
29 | subtitle: '...',
30 | icon: plugin.icon
31 | }
32 | window.searchResultsList.innerHTML = ''
33 | window.currentSearchSuggestions = [temporarySuggestion]
34 | renderSuggestions(window.currentSearchSuggestions)
35 |
36 | let data = null;
37 | let suggestion = null;
38 | try {
39 | data = await (await fetch(request)).json();
40 | if (!data.id) throw new Error('Property ID not found on response object');
41 | suggestion = {
42 | keyword: `${data.id}`,
43 | subtitle: 'Copy shortened url to your clipboard.',
44 | action: () => {
45 | copyToClipboard(data.id, ev => {
46 | window.close();
47 | });
48 | },
49 | icon: plugin.icon
50 | };
51 | } catch (e) {
52 | data = {
53 | id: activeTabUrl
54 | };
55 | suggestion = {
56 | keyword: "Sorry, we couldn't shorten your URL at this time.",
57 | subtitle: 'Either this page is not available online, or the API limit has been exceeded.',
58 | icon: plugin.icon
59 | };
60 | }
61 | window.searchResultsList.innerHTML = ''
62 | window.currentSearchSuggestions = [suggestion]
63 | renderSuggestions(window.currentSearchSuggestions)
64 | }
65 |
66 | module.exports = plugin;
--------------------------------------------------------------------------------
/app/plugins/show-all-commands/index.js:
--------------------------------------------------------------------------------
1 | const utils = require('../../util')
2 | const plugin = {
3 | keyword: "Show All Commands",
4 | subtitle: 'Display all available Cato app commands.',
5 | action: getAllSuggestions,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | function getAllSuggestions(){
12 | window.searchResultsList.innerHTML = ""
13 | utils.renderSuggestions(window.defaultSuggestions)
14 | window.searchInput.value = ''
15 | window.suggestionElements = document.querySelectorAll('.cLauncher__suggestion')
16 | }
17 |
18 | module.exports = plugin
19 |
--------------------------------------------------------------------------------
/app/plugins/sort-tabs/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const domain = require('getdomain')
3 | const sortBy = require('lodash.sortby')
4 |
5 | const plugin = {
6 | keyword: "Sort Tabs",
7 | subtitle: 'Sort tabs by website.',
8 | action: sortTabs,
9 | icon: {
10 | path: 'images/chrome-icon.svg'
11 | }
12 | }
13 |
14 | async function sortTabs() {
15 | const allWindows = await browser.windows.getAll({populate: true})
16 | allWindows.forEach((browserWindow) => {
17 | let browserWindowTabs = []
18 |
19 | browserWindow.tabs.forEach((tab) => {
20 | browserWindowTabs.push({id: tab.id, domain: domain.origin(tab.url)})
21 | })
22 | browserWindowTabs = sortBy(browserWindowTabs, ['domain'])
23 | browserWindowTabs.forEach((tab, index) => browser.tabs.move(tab.id, {index}))
24 | })
25 | window.close()
26 |
27 | }
28 |
29 | module.exports = plugin
30 |
--------------------------------------------------------------------------------
/app/plugins/toggle-bookmark/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: 'Bookmark/Unbookmark page',
4 | subtitle: 'Bookmark the page if not bookmarked, otherwise unbookmark it.',
5 | action: toggleBookmark,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function toggleBookmark() {
12 | const allTabs = await browser.tabs.query({'active': true, currentWindow: true})
13 | const activeTab = allTabs[0]
14 | const matchedBookmarks = await browser.bookmarks.search({url: activeTab.url})
15 |
16 | if (matchedBookmarks.length === 0) {
17 | await browser.bookmarks.create({title: activeTab.title, url: activeTab.url})
18 | }
19 | else {
20 | // apparently, you can bookmark the same page in a different tab.
21 | for (const bookmark of matchedBookmarks) {
22 | await browser.bookmarks.remove(bookmark.id)
23 | }
24 | }
25 | window.close()
26 | }
27 |
28 | module.exports = plugin
29 |
--------------------------------------------------------------------------------
/app/plugins/toggle-fullscreen-mode/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Toggle Fullscreen",
4 | subtitle: 'Turn fullscreen mode on/off for the window.',
5 | action: toggleFullscreenMode,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function toggleFullscreenMode() {
12 | const activeWindow = await browser.windows.get(browser.windows.WINDOW_ID_CURRENT)
13 | const isFullScreen = activeWindow.state === 'fullscreen'
14 |
15 | if (isFullScreen) {
16 | browser.windows.update(browser.windows.WINDOW_ID_CURRENT, {state: "normal"})
17 | }
18 | else {
19 | browser.windows.update(browser.windows.WINDOW_ID_CURRENT, {state: "fullscreen"})
20 | }
21 | }
22 |
23 | module.exports = plugin
24 |
--------------------------------------------------------------------------------
/app/plugins/toggle-mute-tab/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Mute/Unmute Tab",
4 | subtitle: 'Mute the tab if it\'s unmuted otherwise unmute it.',
5 | action: toggleMuteTab,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function toggleMuteTab() {
12 | const allTabs = await browser.tabs.query({'active': true, currentWindow: true})
13 | const activeTab = allTabs[0]
14 | const isMuted = activeTab.mutedInfo.muted
15 |
16 | browser.tabs.update({'muted': !isMuted})
17 | window.close()
18 | }
19 |
20 | module.exports = plugin
21 |
--------------------------------------------------------------------------------
/app/plugins/toggle-mute-tabs/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Mute/Unmute All Tab",
4 | subtitle: 'Mute or unmute all tabs.',
5 | action: toggleMuteAllTabs,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function toggleMuteAllTabs() {
12 | const allWindows = await browser.windows.getAll({populate: true})
13 | allWindows.forEach((browserWindow) => {
14 | browserWindow.tabs.forEach((tab) => {
15 | browser.tabs.update(tab.id, {'muted': !tab.mutedInfo.muted})
16 | })
17 | window.close()
18 | })
19 | }
20 |
21 | module.exports = plugin
22 |
--------------------------------------------------------------------------------
/app/plugins/uninstall-extension/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const utils = require('../../util')
3 | const sortBy = require('lodash.sortby')
4 |
5 | const plugin = {
6 | keyword: "Uninstall Extension",
7 | subtitle: 'Uninstall one of your installed chrome extensions.',
8 | valid: false,
9 | searchScope: 'Uninstall Extension',
10 | action: uninstallExtension,
11 | icon: {
12 | path: 'images/chrome-icon.svg'
13 | }
14 | }
15 | async function uninstallExtension() {
16 | window.searchInput.value = ''
17 | window.searchResultsList.innerHTML = ""
18 |
19 | if (!plugin.valid) {
20 | window.searchInput.value = `${plugin.keyword} `
21 | window.searchResultsList.innerHTML = ""
22 | const extensionsAndApps = await browser.management.getAll()
23 | let extensions = extensionsAndApps
24 | .filter((extensionOrApp) => {
25 | return extensionOrApp.type === 'extension' &&
26 | extensionOrApp.name !== "Awesome Task Launcher"
27 | })
28 |
29 | extensions = sortBy(extensions, ['name'])
30 |
31 | window.currentSearchSuggestions = extensions.map((extension) => {
32 | const action = {
33 | 'keyword': `${extension.name}`,
34 | 'action': uninstall(extension),
35 | 'icon': {
36 | path: utils.useAvalableExtensionIcon(extension)
37 | }
38 | }
39 | return action
40 | })
41 | utils.renderSuggestions(window.currentSearchSuggestions)
42 | window.suggestionElements = document.querySelectorAll('.cLauncher__suggestion')
43 | }
44 |
45 | }
46 |
47 | //Helper
48 | function uninstall(extension) {
49 | return async function closureFunc() {
50 | const options = {showConfirmDialog: true}
51 | await browser.management.uninstall(extension.id, options)
52 | }
53 | }
54 |
55 | module.exports = plugin
56 |
--------------------------------------------------------------------------------
/app/plugins/video-controller/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 |
3 | const togglePlayVideo = {
4 | keyword: "Play/Pause",
5 | subtitle: "Play/Unpause the currently playing video.",
6 | action: playOrPause,
7 | icon: {
8 | path:'images/chrome-icon.svg'
9 | }
10 | }
11 |
12 | async function playOrPause() {
13 | await browser.tabs.executeScript(null, {
14 | code: `
15 | function togglePlayVideo(){
16 | let video = document.querySelector('video');
17 | if(video){
18 | if(video.paused){
19 | video.play();
20 | } else {
21 | video.pause()
22 | }
23 | }
24 | }
25 | togglePlayVideo()
26 | `
27 | })
28 | window.close()
29 | }
30 |
31 |
32 | module.exports = togglePlayVideo
33 |
--------------------------------------------------------------------------------
/app/plugins/view-source-toggle/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "Toggle View Source",
4 | subtitle: 'Toggle between the source and the normal view of the current page',
5 | action: toggleViewSource,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function toggleViewSource() {
12 | const tabs = await browser.tabs.query({active: true, currentWindow: true});
13 | const activeTabId = 0 + tabs[0].id;
14 | const activeTabUrl = tabs[0].url;
15 | const viewSourceString = 'view-source:';
16 |
17 | if (!activeTabUrl.startsWith(viewSourceString)) {
18 | await browser.tabs.update(activeTabId, {
19 | url: viewSourceString+activeTabUrl
20 | });
21 | } else {
22 | await browser.tabs.update(activeTabId, {
23 | url: activeTabUrl.substring(viewSourceString.length)
24 | });
25 | }
26 | window.close();
27 | }
28 |
29 | module.exports = plugin
30 |
--------------------------------------------------------------------------------
/app/plugins/view-source/index.js:
--------------------------------------------------------------------------------
1 | const browser = require('webextension-polyfill')
2 | const plugin = {
3 | keyword: "View Source",
4 | subtitle: 'Shows the source code of the current page',
5 | action: viewSource,
6 | icon: {
7 | path: 'images/chrome-icon.svg'
8 | }
9 | }
10 |
11 | async function viewSource() {
12 | const tabs = await browser.tabs.query({active: true, currentWindow: true});
13 | const activeTabUrl = tabs[0].url;
14 | const viewSourceString = 'view-source:';
15 |
16 | if (!activeTabUrl.startsWith(viewSourceString)) {
17 | await browser.tabs.create({url: viewSourceString+activeTabUrl});
18 | } else {
19 | // If this page is a source page, then just clone it.
20 | await browser.tabs.create({url: activeTabUrl});
21 | }
22 |
23 | }
24 |
25 | module.exports = plugin
26 |
--------------------------------------------------------------------------------
/app/util.js:
--------------------------------------------------------------------------------
1 | const utils = {}
2 | const {h} = require('dom-chef')
3 | const fuzzaldrinPlus = require('fuzzaldrin-plus')
4 | const mathexp = require('math-expression-evaluator')
5 | const browser = require('webextension-polyfill')
6 |
7 | utils.highlightTopSuggestion = () => {
8 | window.searchResultsList.children[0].classList.add("selected")
9 | }
10 |
11 |
12 |
13 | utils.createSuggestionElement = function(suggestion) {
14 | const isObject = Object.prototype.toString.call(suggestion) === '[object Object]' ? true: false
15 | if(isObject) {
16 | const element = (
17 |
18 |
19 |
29 |
30 | )
31 | return element
32 | }
33 | }
34 |
35 |
36 |
37 | utils.renderSuggestions = function (suggestions) {
38 | suggestions.forEach((suggestion) => {
39 | const searchResult = utils.createSuggestionElement(suggestion)
40 | if(searchResult !== undefined) {
41 | window.searchResultsList.appendChild(searchResult)
42 | }
43 | })
44 | utils.highlightTopSuggestion()
45 | }
46 |
47 |
48 |
49 |
50 |
51 | utils.getMatches = function(query, suggestions) {
52 | const matches = fuzzaldrinPlus
53 | .filter(suggestions, query, {key: 'keyword', maxResults: 20})
54 | .map((matchedResult) => {
55 | matchedResult.textWithMatchedChars = fuzzaldrinPlus.wrap(matchedResult.keyword, query)
56 | return matchedResult
57 | })
58 | return matches
59 | }
60 |
61 |
62 |
63 | utils.displayPotentialMathResult = function(query) {
64 | try {
65 | const mathResult = mathexp.eval(query);
66 | return [{
67 | keyword: mathResult,
68 | subtitle: 'Copy number to your clipboard.',
69 | action: function copyResult() {
70 | utils.copyToClipboard(mathResult, ev => {
71 | window.close();
72 | });
73 | },
74 | icon: {
75 | path: 'images/calculator-icon.svg'
76 | }
77 | }]
78 | }
79 | catch(exception) {
80 | if(exception.message === "complete the expression" && query !== '') {
81 | return [{
82 | keyword: '...',
83 | action: '',
84 | subtitle: 'Please enter a valid math expression.',
85 | icon: {
86 | path: 'images/calculator-icon.svg'
87 | }
88 | }]
89 | }
90 | else {
91 | return []
92 | }
93 | }
94 | }
95 |
96 |
97 |
98 |
99 |
100 | /**
101 | * Contains the default configuration for the appearance of the command palette.
102 | * @type {Object}
103 | */
104 | utils.defaultThemeConfig = {
105 | //classic #FFFFFF is the default theme
106 |
107 | // element background colors
108 | "--app-background-color": "#FFFFFF",
109 | "--app-width-size": "500px",
110 | "--search-input-caret-color": "#222222",
111 | "--search-results-scrollbar-color": "#222222",
112 | "--selected-suggestion-background-color": "#222222",
113 |
114 | // text sizing
115 | "--search-input-value-text-size": "30px",
116 | "--suggestion-title-text-size": "16px",
117 | "--suggestion-subtitle-text-size": "14px",
118 |
119 |
120 |
121 | //text colors
122 | "--search-input-value-text-color": "#222222",
123 | "--selected-suggestion-title-text-color": "#FFFFFF",
124 | "--selected-suggestion-subtitle-text-color": "#FFFFFF",
125 | "--selected-suggestion-character-match-color": "#FFFFFF",
126 | "--suggestion-title-text-color": "#222222",
127 | "--suggestion-subtitle-text-color": "#222222",
128 | "--suggestion-character-match-color": "#222222",
129 |
130 | //spacing
131 | "--search-results-scrollbar-width": "2px"
132 | }
133 |
134 |
135 | /**
136 | * Resets the appearance of the launcher to the default color (page variables only, not localstorage)
137 | *@returns {void}
138 | */
139 | utils.resetFakeAppTheme = () => {
140 | //update root variables on the page.
141 | for (const key in utils.defaultThemeConfig) {
142 | if (utils.defaultThemeConfig.hasOwnProperty(key)) {
143 | utils.updateCSSVariable(key, utils.defaultThemeConfig[key])
144 | }
145 | }
146 | }
147 |
148 |
149 | /**
150 | * Resets the theme values in localstorage to our defaults.
151 | *@returns {void}
152 | */
153 | utils.resetLocalStorageTheme = async () => {
154 | await browser.storage.sync.set({themeConfig: utils.defaultThemeConfig})
155 | }
156 |
157 |
158 | /**
159 | * Resets the theme input value back to the default value defined in our utilities (page only, not localstorage)
160 | * @param {HTMLElement} input an input element
161 | */
162 | utils.resetThemeInputValue = (input) => {
163 | const themeProperty = input.dataset.cssvariable
164 | let themeConfigValue = utils.defaultThemeConfig[themeProperty]
165 |
166 | if (input.type === 'number') {
167 | themeConfigValue = themeConfigValue.slice(0, -2) // 500px -> 500
168 | }
169 | //update the value attribute in the markup to reflect changes (not the live value)
170 | //update the value 'property' on the element (contains live value)
171 | input.setAttribute('value', themeConfigValue)
172 | input.value = themeConfigValue
173 | }
174 |
175 | /**
176 | * Updates a CSS variable's value.
177 | * @param {string} propertyName
178 | * @param {string} value
179 | * @param {HTMLElement} element the element we want to append/modify css to.
180 | */
181 | utils.updateCSSVariable = (propertyName, value, element = document.documentElement) => {
182 | element.style.setProperty(propertyName, value)
183 | }
184 |
185 |
186 | /**
187 | *@description
188 | *Update the 'value' property on an input value on change, then updates the root css variable values on the page.
189 | */
190 | utils.handleThemeInputValueChanges = async (event) => {
191 | const catoCommandName = window.selectedElement.querySelector('.cLauncher__suggestion-title').innerText; //This is not the value inside of the search input box.
192 | window.selectedElement.click();
193 | //https://developers.google.com/analytics/devguides/collection/analyticsjs/events
194 | ga('send',
195 | 'event',
196 | 'changeThemeInput',
197 | 'changeThemeInput'
198 | );
199 | const element = event.target
200 | const valueSuffix = element.dataset.sizing || ''
201 | const cssProperty = element.dataset.cssvariable
202 | const cssPropertyValue = element.value + valueSuffix
203 | const update = {[cssProperty]: cssPropertyValue}
204 |
205 | utils.updateCSSVariable(`${cssProperty}`, cssPropertyValue)
206 | //update local storage
207 | const {themeConfig} = await browser.storage.sync.get('themeConfig')
208 | const newThemeConfig = Object.assign(themeConfig, {[cssProperty]: cssPropertyValue})
209 | await browser.storage.sync.set({themeConfig: newThemeConfig})
210 |
211 | }
212 |
213 | utils.useAvalableExtensionIcon = function useAvalableExtensionIcon(extension) {
214 | if(typeof extension.icons !== 'object') {
215 | return 'images/blank-page-icon.svg' }
216 | const icon = extension.icons[3] || extension.icons[2] || extension.icons[1] || extension.icons[0]
217 | return icon.url
218 | }
219 |
220 | utils.copyToClipboard = function copyToClipboard(value, cb = function() {} ) {
221 | document.addEventListener('copy', (event) => {
222 | // Prevents the default behavior of copying, ex: pressing Ctrl+C
223 | // If we didn't prevent the prevent default, the clipboard would be filled with what ever the user had highlighted on the page.
224 | event.preventDefault();
225 | event.clipboardData.setData('text/plain', value);
226 | cb(event);
227 | }, { once: true })
228 | document.execCommand('copy');
229 | }
230 |
231 | module.exports = utils
232 |
--------------------------------------------------------------------------------
/media/cato-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/cato-logo.png
--------------------------------------------------------------------------------
/media/change-shortcut-how-to.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/change-shortcut-how-to.png
--------------------------------------------------------------------------------
/media/chrome-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/chrome-icon.png
--------------------------------------------------------------------------------
/media/example-calculator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/example-calculator.png
--------------------------------------------------------------------------------
/media/example-change-tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/example-change-tab.png
--------------------------------------------------------------------------------
/media/example-command-showcase2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/example-command-showcase2.png
--------------------------------------------------------------------------------
/media/example-commands-showcase1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/example-commands-showcase1.png
--------------------------------------------------------------------------------
/media/example-disable-extension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/example-disable-extension.png
--------------------------------------------------------------------------------
/media/example-enable-extension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/example-enable-extension.png
--------------------------------------------------------------------------------
/media/example-fallback-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/example-fallback-search.png
--------------------------------------------------------------------------------
/media/example-theme-customizer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/example-theme-customizer.png
--------------------------------------------------------------------------------
/media/feature-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/feature-banner.png
--------------------------------------------------------------------------------
/media/opera-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/opera-icon.png
--------------------------------------------------------------------------------
/media/usage-example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cliffordfajardo/cato/7b588e7d3024e41840412fae721f569e00689a34/media/usage-example.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-fuzzy",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "build": "webpack",
7 | "dev": "npm run watch",
8 | "watch": "webpack --watch",
9 | "build:minified": "cross-env NODE_ENV=production webpack",
10 | "cleaninstall": "rm -rf node_modules && npm i"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "babel-runtime": "^6.26.0",
16 | "bulma": "^0.5.3",
17 | "dom-chef": "^2.0.0",
18 | "fuzzaldrin-plus": "^0.5.0",
19 | "getdomain": "^1.0.7",
20 | "grapnel": "^0.6.4",
21 | "lodash.sortby": "^4.7.0",
22 | "math-expression-evaluator": "^1.2.17",
23 | "normalize.css": "^7.0.0"
24 | },
25 | "devDependencies": {
26 | "babel-cli": "^6.26.0",
27 | "babel-core": "^6.26.0",
28 | "babel-loader": "^7.1.2",
29 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
30 | "babel-plugin-transform-react-jsx": "^6.24.1",
31 | "babel-plugin-transform-runtime": "^6.23.0",
32 | "babel-preset-env": "^1.6.0",
33 | "babel-preset-es2015": "^6.24.1",
34 | "copy-webpack-plugin": "^4.0.1",
35 | "cross-env": "^5.0.5",
36 | "css-loader": "^0.28.7",
37 | "cssnano": "^3.10.0",
38 | "eslint": "^4.7.2",
39 | "extract-text-webpack-plugin": "^3.0.0",
40 | "file-loader": "^0.11.2",
41 | "html-loader": "^0.5.1",
42 | "html-webpack-plugin": "^2.30.1",
43 | "imagemin-webpack-plugin": "^1.5.2",
44 | "node-sass": "^4.5.3",
45 | "optimize-css-assets-webpack-plugin": "^3.2.0",
46 | "sass-loader": "^6.0.6",
47 | "style-loader": "^0.18.2",
48 | "uglifyjs-webpack-plugin": "^1.0.0-beta.2",
49 | "webextension-polyfill": "^0.1.1",
50 | "webpack": "^3.6.0"
51 | },
52 | "babel": {
53 | "presets": [
54 | [
55 | "env",
56 | {
57 | "targets": {
58 | "chrome": 49
59 | }
60 | }
61 | ]
62 | ],
63 | "plugins": [
64 | [
65 | "transform-react-jsx",
66 | {
67 | "pragma": "h",
68 | "useBuiltIns": true
69 | }
70 | ],
71 | [
72 | "transform-runtime",
73 | {
74 | "polyfill": true,
75 | "regenerator": true
76 | }
77 | ]
78 | ]
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 | const ExtractTextPlugin = require("extract-text-webpack-plugin")
4 | const webpack = require('webpack')
5 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
6 | const CopyWebpackPlugin = require('copy-webpack-plugin');
7 | const ImageminPlugin = require('imagemin-webpack-plugin').default
8 | const cssNano = require('cssnano')
9 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
10 | const isProduction = process.env.NODE_ENV === 'production'
11 |
12 | const config = {
13 | entry: {
14 | popup: './app/pages/popup/popup.js',
15 | options: './app/pages/options/options.js'
16 | },
17 | output: {
18 | path: path.resolve(__dirname, './extension'),
19 | filename: '[name].js'
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.html$/,
25 | use: ['html-loader']
26 | },
27 | {
28 | test: /\.css$/,
29 | use: ['style-loader', 'css-loader']
30 | },
31 | {
32 | test: /\.js$/,
33 | exclude: /node_modules/,
34 | use: ['babel-loader']
35 | },
36 | {
37 | test: /\.scss$/,
38 | use: ExtractTextPlugin.extract({
39 | fallback: 'style-loader',
40 | use: ['css-loader', 'sass-loader']
41 | })
42 | },
43 | {
44 | test: /\.(jpe?g|png|gif|svg)$/i,
45 | use: [
46 | 'file-loader?name=[name].[ext]&outputPath=images/&publicPath=images/'
47 | ]
48 | }
49 | ]
50 | },
51 | plugins: [
52 | new webpack.optimize.ModuleConcatenationPlugin(),
53 | //Generate an HTML5 file that includes all webpack bundles(includes css & js) in the body using script tags
54 | new HtmlWebpackPlugin({
55 | title: 'Cato - App',
56 | template: './app/pages/popup/popup.html',
57 | filename: 'popup.html',
58 | chunks: ['popup']
59 | }),
60 | new HtmlWebpackPlugin({
61 | title: 'Cato - Options',
62 | template: './app/pages/options/options.html',
63 | filename: 'options.html',
64 | chunks: ['options']
65 | }),
66 | //Create our CSS bundles by our entry points names (Ex: popup.css, options.css)
67 | new ExtractTextPlugin({
68 | filename: '[name].css'
69 | }),
70 | new CopyWebpackPlugin([
71 | {from: 'app/images', to: 'images'},
72 | {from: 'app/manifest.json'}
73 | ]),
74 | new ImageminPlugin({test: /\.(jpe?g|png|gif|svg)$/i})
75 | ]
76 | }
77 |
78 | if(isProduction) {
79 | config.plugins.push(
80 | new UglifyJSPlugin({
81 | sourceMap: false,
82 | uglifyOptions: {
83 | compress: {
84 | dead_code: true,
85 | drop_console: true,
86 | unused: true,
87 | if_return: true
88 | }
89 | }
90 | }),
91 | new OptimizeCssAssetsPlugin({
92 | assetNameRegExp: /\.css$/,
93 | cssProcessor: cssNano,
94 | cssProcessorOptions: {discardComments: {removeAll: true}}, canPrint: true
95 | })
96 | )
97 | }
98 |
99 | module.exports = config
100 |
--------------------------------------------------------------------------------