├── .eslintrc ├── LICENSE ├── babel.config.json ├── config ├── jest │ ├── afterEnv.js │ ├── globalSetup.js │ ├── globalTeardown.js │ └── testServer.js ├── webpack.config.js └── webpack.test.config.js ├── firefox_pac.js ├── jest.config.js ├── package.json ├── sk.svg ├── sk.xcf ├── src ├── background │ ├── chrome.js │ ├── firefox.js │ ├── llm.js │ ├── safari.js │ └── start.js ├── common │ └── utils.js ├── content_scripts │ ├── ace.js │ ├── chrome.js │ ├── common │ │ ├── api.js │ │ ├── clipboard.js │ │ ├── cursorPrompt.js │ │ ├── debug_utils.js │ │ ├── default.js │ │ ├── hints.js │ │ ├── insert.js │ │ ├── keyboardUtils.js │ │ ├── mode.js │ │ ├── normal.js │ │ ├── observer.js │ │ ├── runtime.js │ │ ├── trie.js │ │ ├── utils.js │ │ └── visual.js │ ├── content.css │ ├── content.js │ ├── firefox.js │ ├── front.js │ ├── gist.js │ ├── markdown.js │ ├── options.js │ ├── start.js │ ├── ui │ │ ├── command.js │ │ ├── frontend.css │ │ ├── frontend.html │ │ ├── frontend.js │ │ ├── llmchat.js │ │ └── omnibar.js │ └── uiframe.js ├── icons │ ├── 128.png │ ├── 16.png │ ├── 48-l.png │ ├── 48-x.png │ └── 48.png ├── manifest.json ├── nvim │ ├── Nvim.ts │ ├── __generated__ │ │ ├── constants.ts │ │ └── types.ts │ ├── features │ │ └── hideMouseCursor.ts │ ├── input │ │ ├── keyboard.ts │ │ └── mouse.ts │ ├── lib │ │ ├── getColor.ts │ │ └── pixi.ts │ ├── renderer.ts │ ├── screen.ts │ ├── server │ │ ├── NativeMessagingHosts │ │ │ └── Surfingkeys.json │ │ ├── Surfingkeys.reg │ │ ├── server.lua │ │ ├── start.bat │ │ ├── start.sh │ │ └── start_none.sh │ ├── transport │ │ └── websocket.ts │ └── types.ts ├── pages │ ├── common.css │ ├── donation.png │ ├── emoji.tsv │ ├── images │ │ ├── altText_add.svg │ │ ├── altText_disclaimer.svg │ │ ├── altText_done.svg │ │ ├── altText_spinner.svg │ │ ├── altText_warning.svg │ │ ├── annotation-check.svg │ │ ├── annotation-comment.svg │ │ ├── annotation-help.svg │ │ ├── annotation-insert.svg │ │ ├── annotation-key.svg │ │ ├── annotation-newparagraph.svg │ │ ├── annotation-noicon.svg │ │ ├── annotation-note.svg │ │ ├── annotation-paperclip.svg │ │ ├── annotation-paragraph.svg │ │ ├── annotation-pushpin.svg │ │ ├── cursor-editorFreeHighlight.svg │ │ ├── cursor-editorFreeText.svg │ │ ├── cursor-editorInk.svg │ │ ├── cursor-editorTextHighlight.svg │ │ ├── editor-toolbar-delete.svg │ │ ├── findbarButton-next.svg │ │ ├── findbarButton-previous.svg │ │ ├── gv-toolbarButton-download.svg │ │ ├── loading-icon.gif │ │ ├── loading.svg │ │ ├── messageBar_closingButton.svg │ │ ├── messageBar_warning.svg │ │ ├── secondaryToolbarButton-documentProperties.svg │ │ ├── secondaryToolbarButton-firstPage.svg │ │ ├── secondaryToolbarButton-handTool.svg │ │ ├── secondaryToolbarButton-lastPage.svg │ │ ├── secondaryToolbarButton-rotateCcw.svg │ │ ├── secondaryToolbarButton-rotateCw.svg │ │ ├── secondaryToolbarButton-scrollHorizontal.svg │ │ ├── secondaryToolbarButton-scrollPage.svg │ │ ├── secondaryToolbarButton-scrollVertical.svg │ │ ├── secondaryToolbarButton-scrollWrapped.svg │ │ ├── secondaryToolbarButton-selectTool.svg │ │ ├── secondaryToolbarButton-spreadEven.svg │ │ ├── secondaryToolbarButton-spreadNone.svg │ │ ├── secondaryToolbarButton-spreadOdd.svg │ │ ├── toolbarButton-bookmark.svg │ │ ├── toolbarButton-currentOutlineItem.svg │ │ ├── toolbarButton-download.svg │ │ ├── toolbarButton-editorFreeText.svg │ │ ├── toolbarButton-editorHighlight.svg │ │ ├── toolbarButton-editorInk.svg │ │ ├── toolbarButton-editorStamp.svg │ │ ├── toolbarButton-menuArrow.svg │ │ ├── toolbarButton-openFile.svg │ │ ├── toolbarButton-pageDown.svg │ │ ├── toolbarButton-pageUp.svg │ │ ├── toolbarButton-presentationMode.svg │ │ ├── toolbarButton-print.svg │ │ ├── toolbarButton-search.svg │ │ ├── toolbarButton-secondaryToolbarToggle.svg │ │ ├── toolbarButton-sidebarToggle.svg │ │ ├── toolbarButton-viewAttachments.svg │ │ ├── toolbarButton-viewLayers.svg │ │ ├── toolbarButton-viewOutline.svg │ │ ├── toolbarButton-viewThumbnail.svg │ │ ├── toolbarButton-zoomIn.svg │ │ ├── toolbarButton-zoomOut.svg │ │ ├── treeitem-collapsed.svg │ │ └── treeitem-expanded.svg │ ├── l10n.json │ ├── markdown.css │ ├── markdown.html │ ├── neovim.html │ ├── neovim.js │ ├── options.css │ ├── options.html │ ├── pdf_viewer.css │ ├── pdf_viewer.html │ ├── pdf_viewer.mjs │ ├── popup.html │ ├── popup.js │ ├── shadow.css │ ├── start.css │ └── start.html └── user_scripts │ └── index.js ├── tests ├── background │ └── start.test.js ├── content_scripts │ ├── common │ │ └── normal.test.js │ ├── markdown.test.js │ ├── ui │ │ ├── frontend.test.js │ │ └── omnibar.test.js │ └── uiframe.test.js ├── nvim │ ├── Nvim.test.ts │ ├── __image_snapshots__ │ │ ├── screen-test-ts-screen-match-snapshot-1-snap.png │ │ ├── screen-test-ts-screen-redraw-screen-on-default-colors-set-1-snap.png │ │ └── screen-test-ts-screen-undercurl-show-undercurl-behind-the-text-1-snap.png │ ├── getColor.test.ts │ ├── keyboard.test.ts │ ├── renderer.test.ts │ └── screen.ts └── utils.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "semi": ["error", "always"], 9 | "no-tabs": 2 10 | } 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 brookhong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "modules": "commonjs" }], "@babel/preset-typescript"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime", 5 | [ 6 | "module-resolver", 7 | { 8 | "root": ["."], 9 | "alias": { 10 | "src": "./src" 11 | } 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/afterEnv.js: -------------------------------------------------------------------------------- 1 | const { toMatchImageSnapshot } = require('jest-image-snapshot'); 2 | 3 | expect.extend({ toMatchImageSnapshot }); 4 | -------------------------------------------------------------------------------- /config/jest/globalSetup.js: -------------------------------------------------------------------------------- 1 | import { setupTestServer } from './testServer'; 2 | 3 | const globalSetup = async () => { 4 | await setupTestServer(); 5 | }; 6 | 7 | export default globalSetup; 8 | -------------------------------------------------------------------------------- /config/jest/globalTeardown.js: -------------------------------------------------------------------------------- 1 | import { teardownTestServer } from './testServer'; 2 | 3 | const globalTeardown = async () => { 4 | await teardownTestServer(); 5 | }; 6 | 7 | export default globalTeardown; 8 | -------------------------------------------------------------------------------- /config/jest/testServer.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | export const setupTestServer = async () => { 4 | spawn('npm', [ 5 | 'run', 6 | 'build:testdata' 7 | ]); 8 | }; 9 | 10 | export const teardownTestServer = async () => { 11 | }; 12 | -------------------------------------------------------------------------------- /config/webpack.test.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const buildPath = path.resolve(__dirname, '../dist/testdata'); 3 | 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | 6 | function modifyMH(browser, mode, buffer) { 7 | // copy-webpack-plugin passes a buffer 8 | var manifest = JSON.parse(buffer.toString()); 9 | 10 | manifest.path = path.resolve(__dirname, '../src/nvim/server/start_none.sh'); 11 | 12 | // pretty print to JSON with two spaces 13 | manifest_JSON = JSON.stringify(manifest, null, 2); 14 | return manifest_JSON; 15 | } 16 | 17 | module.exports = (env, argv) => { 18 | const mode = argv.mode; 19 | const browser = env.browser ? env.browser : 'chrome'; 20 | return { 21 | devtool: false, 22 | output: { 23 | path: buildPath, 24 | filename: '[name].js', 25 | }, 26 | resolve: { 27 | extensions: ['.ts', '.js'], 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.ts$/, 33 | exclude: /node_modules/, 34 | loader: 'ts-loader', 35 | }, 36 | ], 37 | }, 38 | target: ['web', 'es5'], 39 | entry: { 40 | }, 41 | plugins: [ 42 | new CopyWebpackPlugin({ 43 | patterns: [ 44 | { 45 | from: "src/nvim/server/NativeMessagingHosts/Surfingkeys.json", 46 | to: "./NativeMessagingHosts", 47 | transform (content, path) { 48 | return modifyMH(browser, mode, content) 49 | } 50 | } 51 | ] 52 | }) 53 | ] 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /firefox_pac.js: -------------------------------------------------------------------------------- 1 | var pacGlobal = {}; 2 | 3 | browser.runtime.onMessage.addListener((message) => { 4 | pacGlobal = message; 5 | pacGlobal.proxy.forEach(function(proxy, i) { 6 | if (proxy.toLowerCase().indexOf("socks") === 0) { 7 | var p = proxy.split(" "); 8 | var h = p[1].split(":"); 9 | pacGlobal.proxy[i] = [{ 10 | type: p[0], 11 | host: h[0], 12 | port: h[1], 13 | proxyDNS: true 14 | }]; 15 | } 16 | }); 17 | }); 18 | 19 | function FindProxyForURL(url, host) { 20 | var lastPos; 21 | if (pacGlobal.proxyMode === "always") { 22 | return pacGlobal.proxy[0]; 23 | } else if (pacGlobal.proxyMode === "bypass") { 24 | var pp = new RegExp(pacGlobal.autoproxy_pattern[0]); 25 | do { 26 | if (pacGlobal.hosts[0].hasOwnProperty(host) 27 | || (pacGlobal.autoproxy_pattern[0].length && pp.test(host))) { 28 | return "DIRECT"; 29 | } 30 | lastPos = host.indexOf('.') + 1; 31 | host = host.slice(lastPos); 32 | } while (lastPos >= 1); 33 | return pacGlobal.proxy[0]; 34 | } else { 35 | for (var i = 0; i < pacGlobal.proxy.length; i++) { 36 | var pp = new RegExp(pacGlobal.autoproxy_pattern[i]); 37 | var ahost = host; 38 | do { 39 | if (pacGlobal.hosts[i].hasOwnProperty(ahost) 40 | || (pacGlobal.autoproxy_pattern[i].length && pp.test(ahost))) { 41 | return pacGlobal.proxy[i]; 42 | } 43 | lastPos = ahost.indexOf('.') + 1; 44 | ahost = ahost.slice(lastPos); 45 | } while (lastPos >= 1); 46 | } 47 | return "DIRECT"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | clearMocks: true, 4 | collectCoverage: true, 5 | collectCoverageFrom: ['src/**/*.{ts,js}'], 6 | setupFilesAfterEnv: ['/config/jest/afterEnv.js'], 7 | globalSetup: '/config/jest/globalSetup.js', 8 | globalTeardown: '/config/jest/globalTeardown.js', 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Surfingkeys", 3 | "version": "1.17.9", 4 | "description": "Map your keys for web surfing, expand your browser with javascript and keyboard.", 5 | "main": "background.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "tests" 9 | }, 10 | "scripts": { 11 | "clean": "rm -rf dist/*", 12 | "build:doc": "documentation build src/content_scripts/common/api.js src/content_scripts/common/normal.js src/content_scripts/common/clipboard.js src/content_scripts/common/hints.js src/content_scripts/common/visual.js src/content_scripts/front.js src/user_scripts/index.js -f md -o docs/api.md", 13 | "build:dev": "webpack --mode=development --config ./config/webpack.config.js", 14 | "build:prod": "webpack --mode=production --config ./config/webpack.config.js", 15 | "build:testdata": "webpack --mode=production --config ./config/webpack.test.config.js", 16 | "build": "npm-run-all clean test build:doc build:prod", 17 | "test": "jest" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/brookhong/Surfingkeys.git" 22 | }, 23 | "author": "brook hong", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/brookhong/Surfingkeys/issues" 27 | }, 28 | "homepage": "https://github.com/brookhong/Surfingkeys#readme", 29 | "devDependencies": { 30 | "@babel/plugin-proposal-class-properties": "^7.14.5", 31 | "@babel/plugin-proposal-optional-chaining": "^7.14.5", 32 | "@babel/plugin-transform-runtime": "^7.15.0", 33 | "@babel/preset-env": "^7.15.0", 34 | "@babel/preset-typescript": "^7.15.0", 35 | "@types/events": "^3.0.0", 36 | "@types/jest-image-snapshot": "^4.3.1", 37 | "@types/lodash": "^4.14.172", 38 | "@types/offscreencanvas": "^2019.6.4", 39 | "babel-plugin-module-resolver": "^4.1.0", 40 | "copy-webpack-plugin": "^9.0.1", 41 | "documentation": "^13.2.5", 42 | "file-loader": "^6.2.0", 43 | "filemanager-webpack-plugin": "^8.0.0", 44 | "jest": "^27.3.1", 45 | "jest-image-snapshot": "^4.5.1", 46 | "npm-run-all": "^4.1.5", 47 | "puppeteer": "^10.2.0", 48 | "strict-event-emitter-types": "^2.0.0", 49 | "string-replace-loader": "^3.0.3", 50 | "style-loader": "^3.2.1", 51 | "ts-jest": "^27.0.4", 52 | "ts-loader": "^9.2.5", 53 | "typescript": "^4.5.4", 54 | "webpack": "^5.50.0", 55 | "webpack-cli": "^4.8.0" 56 | }, 57 | "dependencies": { 58 | "@msgpack/msgpack": "^2.7.0", 59 | "@pixi/app": "^7.4.0", 60 | "@pixi/constants": "^7.4.0", 61 | "@pixi/core": "^7.4.0", 62 | "@pixi/display": "^7.4.0", 63 | "@pixi/graphics": "^7.4.0", 64 | "@pixi/math": "^7.4.0", 65 | "@pixi/runner": "^7.4.0", 66 | "@pixi/settings": "^7.4.0", 67 | "@pixi/sprite": "^7.4.0", 68 | "@pixi/ticker": "^7.4.0", 69 | "@pixi/unsafe-eval": "^7.4.0", 70 | "@pixi/utils": "^7.4.0", 71 | "@pixi/extensions": "^7.4.0", 72 | "ace-builds": "^1.4.12", 73 | "aws4fetch": "^1.0.20", 74 | "dompurify": "^3.2.4", 75 | "js-base64": "^3.7.2", 76 | "lodash": "^4.17.21", 77 | "marked": "^4.0.10", 78 | "pdfjs-dist": "4.8.69" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sk.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/sk.xcf -------------------------------------------------------------------------------- /src/background/firefox.js: -------------------------------------------------------------------------------- 1 | import { 2 | extendObject, 3 | getSubSettings, 4 | start 5 | } from './start.js'; 6 | 7 | function loadRawSettings(keys, cb, defaultSet) { 8 | var rawSet = defaultSet || {}; 9 | chrome.storage.local.get(null, function(localSet) { 10 | var localSavedAt = localSet.savedAt || 0; 11 | extendObject(rawSet, localSet); 12 | var subset = getSubSettings(rawSet, keys); 13 | if (chrome.runtime.lastError) { 14 | subset.error = "Settings sync may not work thoroughly because of: " + chrome.runtime.lastError.message; 15 | } 16 | cb(subset); 17 | }); 18 | } 19 | 20 | function _applyProxySettings(proxyConf) { 21 | } 22 | 23 | function _setNewTabUrl(){ 24 | return "about:newtab"; 25 | } 26 | 27 | function _getContainerName(self, _response) { 28 | return function (message, sender, sendResponse){ 29 | var cookieStoreId = sender.tab.cookieStoreId; 30 | browser.contextualIdentities.get(cookieStoreId).then(function(container){ 31 | _response(message, sendResponse, { 32 | name : container.name 33 | }); 34 | }, function(err){ 35 | _response(message, sendResponse, { 36 | name : null 37 | });}); 38 | }; 39 | } 40 | 41 | function getLatestHistoryItem(text, maxResults, cb) { 42 | chrome.history.search({ 43 | startTime: 0, 44 | text, 45 | maxResults 46 | }, function(items) { 47 | cb(items); 48 | }); 49 | } 50 | 51 | start({ 52 | detectTabTitleChange: true, 53 | getLatestHistoryItem, 54 | loadRawSettings, 55 | _applyProxySettings, 56 | _setNewTabUrl, 57 | _getContainerName 58 | }); 59 | -------------------------------------------------------------------------------- /src/background/safari.js: -------------------------------------------------------------------------------- 1 | import { 2 | _save, 3 | dictFromArray, 4 | extendObject, 5 | getSubSettings, 6 | start 7 | } from './start.js'; 8 | 9 | function loadRawSettings(keys, cb, defaultSet) { 10 | var rawSet = defaultSet || {}; 11 | chrome.storage.local.get(null, function(localSet) { 12 | var localSavedAt = localSet.savedAt || 0; 13 | chrome.storage.sync.get(null, function(syncSet) { 14 | var syncSavedAt = syncSet.savedAt || 0; 15 | if (localSavedAt > syncSavedAt) { 16 | extendObject(rawSet, localSet); 17 | _save(chrome.storage.sync, localSet, function() { 18 | var subset = getSubSettings(rawSet, keys); 19 | if (chrome.runtime.lastError) { 20 | subset.error = "Settings sync may not work thoroughly because of: " + chrome.runtime.lastError.message; 21 | } 22 | cb(subset); 23 | }); 24 | } else if (localSavedAt < syncSavedAt) { 25 | extendObject(rawSet, syncSet); 26 | cb(getSubSettings(rawSet, keys)); 27 | _save(chrome.storage.local, syncSet); 28 | } else { 29 | extendObject(rawSet, localSet); 30 | cb(getSubSettings(rawSet, keys)); 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | function _applyProxySettings(proxyConf) { 37 | } 38 | 39 | function _setNewTabUrl(){ 40 | return "favorites://"; 41 | } 42 | 43 | function _getContainerName(self, _response){ 44 | } 45 | 46 | function getLatestHistoryItem(text, maxResults, cb) { 47 | } 48 | 49 | start({ 50 | getLatestHistoryItem, 51 | loadRawSettings, 52 | _applyProxySettings, 53 | _setNewTabUrl, 54 | _getContainerName 55 | }); 56 | -------------------------------------------------------------------------------- /src/common/utils.js: -------------------------------------------------------------------------------- 1 | function LOG(level, msg) { 2 | // To turn on all levels: chrome.storage.local.set({"logLevels": ["log", "warn", "error"]}) 3 | chrome.storage.local.get(["logLevels"], (r) => { 4 | const logLevels = r && r.logLevels || ["error"]; 5 | if (["log", "warn", "error"].indexOf(level) !== -1 && logLevels.indexOf(level) !== -1) { 6 | console[level](msg); 7 | } 8 | }); 9 | } 10 | 11 | function regexFromString(str, caseSensitive, highlight) { 12 | var rxp = null; 13 | const flags = caseSensitive ? "" : "i"; 14 | str = str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); 15 | if (highlight) { 16 | rxp = new RegExp(str.replace(/\s+/, "\|"), flags); 17 | } else { 18 | var words = str.split(/\s+/).map(function(w) { 19 | return `(?=.*${w})`; 20 | }).join(''); 21 | rxp = new RegExp(`^${words}.*$`, flags); 22 | } 23 | return rxp; 24 | } 25 | 26 | function filterByTitleOrUrl(urls, query, caseSensitive) { 27 | if (query && query.length) { 28 | var rxp = regexFromString(query, caseSensitive, false); 29 | urls = urls.filter(function(b) { 30 | return rxp.test(b.title) || rxp.test(b.url); 31 | }); 32 | } 33 | return urls; 34 | } 35 | 36 | export { 37 | LOG, 38 | filterByTitleOrUrl, 39 | regexFromString, 40 | } 41 | -------------------------------------------------------------------------------- /src/content_scripts/ace.js: -------------------------------------------------------------------------------- 1 | import "ace-builds/src-noconflict/ace"; 2 | import "ace-builds/src-noconflict/ext-language_tools"; 3 | import "ace-builds/src-noconflict/keybinding-vim"; 4 | import "ace-builds/src-noconflict/mode-javascript"; 5 | import "ace-builds/src-noconflict/theme-chrome"; 6 | -------------------------------------------------------------------------------- /src/content_scripts/chrome.js: -------------------------------------------------------------------------------- 1 | import { 2 | showPopup, 3 | } from './common/utils.js'; 4 | import { dispatchSKEvent, runtime, RUNTIME } from './common/runtime.js'; 5 | import { start } from './content.js'; 6 | 7 | function usePdfViewer() { 8 | window.location.replace(chrome.runtime.getURL("/pages/pdf_viewer.html") + "?file=" + document.URL); 9 | } 10 | 11 | function readText(text, options) { 12 | options = options || { 13 | enqueue: true, 14 | voiceName: runtime.conf.defaultVoice 15 | }; 16 | var verbose = options.verbose; 17 | var stopPattern = /[\s\u00a0]/g, 18 | verbose = options.verbose, 19 | onEnd = options.onEnd; 20 | delete options.verbose; 21 | delete options.onEnd; 22 | RUNTIME('read', { 23 | content: text, 24 | options: options 25 | }, function(res) { 26 | if (verbose) { 27 | if (res.ttsEvent.type === "start") { 28 | showPopup(text); 29 | } else if (res.ttsEvent.type === "word") { 30 | stopPattern.lastIndex = res.ttsEvent.charIndex; 31 | var updated, end = stopPattern.exec(text); 32 | if (end) { 33 | updated = text.substr(0, res.ttsEvent.charIndex) 34 | + "" 35 | + text.substr(res.ttsEvent.charIndex, end.index - res.ttsEvent.charIndex + 1) 36 | + "" 37 | + text.substr(end.index); 38 | } else { 39 | updated = text.substr(0, res.ttsEvent.charIndex) 40 | + "" 41 | + text.substr(res.ttsEvent.charIndex) 42 | + ""; 43 | } 44 | showPopup(updated); 45 | } else if (res.ttsEvent.type === "end") { 46 | dispatchSKEvent("front", ['hidePopup']); 47 | } 48 | } 49 | if (onEnd && (res.ttsEvent.type === "end" || res.ttsEvent.type === "interrupted")) { 50 | onEnd(); 51 | } 52 | return res.ttsEvent.type !== "end"; 53 | }); 54 | } 55 | 56 | start({ 57 | usePdfViewer, 58 | readText 59 | }); 60 | -------------------------------------------------------------------------------- /src/content_scripts/common/clipboard.js: -------------------------------------------------------------------------------- 1 | import { RUNTIME } from './runtime.js'; 2 | import { 3 | actionWithSelectionPreserved, 4 | getBrowserName, 5 | setSanitizedContent, 6 | showBanner, 7 | } from './utils.js'; 8 | 9 | function createClipboard() { 10 | var self = {}; 11 | 12 | var holder = document.createElement('textarea'); 13 | holder.contentEditable = true; 14 | holder.enableAutoFocus = true; 15 | holder.id = 'sk_clipboard'; 16 | 17 | function clipboardActionWithSelectionPreserved(cb) { 18 | actionWithSelectionPreserved(function(selection) { 19 | // avoid editable body 20 | document.documentElement.appendChild(holder); 21 | 22 | cb(selection); 23 | 24 | holder.remove(); 25 | }); 26 | } 27 | 28 | /** 29 | * Read from clipboard. 30 | * 31 | * @param {function} onReady a callback function to handle text read from clipboard. 32 | * @name Clipboard.read 33 | * 34 | * @example 35 | * Clipboard.read(function(response) { 36 | * console.log(response.data); 37 | * }); 38 | */ 39 | self.read = function(onReady) { 40 | if (getBrowserName().startsWith("Safari")) { 41 | RUNTIME('readClipboard', null, function(response) { 42 | onReady(response); 43 | }); 44 | return; 45 | } 46 | 47 | if (getBrowserName() === "Firefox" && 48 | typeof navigator.clipboard === 'object' && typeof navigator.clipboard.readText === 'function') { 49 | navigator.clipboard.readText().then((data) => { 50 | // call back onReady in a different thread to avoid breaking UI operations 51 | // such as Front.openOmnibar 52 | setTimeout(function() { 53 | onReady({ data }); 54 | }, 0); 55 | }); 56 | return; 57 | } 58 | clipboardActionWithSelectionPreserved(function() { 59 | holder.value = ''; 60 | setSanitizedContent(holder, ''); 61 | holder.focus(); 62 | document.execCommand("paste"); 63 | }); 64 | var data = holder.value; 65 | if (data === "") { 66 | data = holder.innerHTML.replace(/
/gi,"\n"); 67 | } 68 | onReady({data: data}); 69 | }; 70 | 71 | /** 72 | * Write text to clipboard. 73 | * 74 | * @param {string} text the text to be written to clipboard. 75 | * @name Clipboard.write 76 | * 77 | * @example 78 | * Clipboard.write(window.location.href); 79 | */ 80 | self.write = function(text) { 81 | const cb = () => { 82 | showBanner("Copied: " + text); 83 | }; 84 | // navigator.clipboard.writeText does not work on http site, and in chrome's background script. 85 | if (getBrowserName() === "Chrome") { 86 | clipboardActionWithSelectionPreserved(function() { 87 | holder.value = text; 88 | holder.select(); 89 | document.execCommand('copy'); 90 | holder.value = ''; 91 | }); 92 | cb(); 93 | } else { 94 | // works for Firefox and Safari now. 95 | RUNTIME("writeClipboard", { text }); 96 | cb(); 97 | } 98 | }; 99 | 100 | return self; 101 | 102 | } 103 | 104 | export default createClipboard; 105 | -------------------------------------------------------------------------------- /src/content_scripts/common/debug_utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * breakOn("aa.scrollTop", 1) break when aa.scrollTop is changed 4 | * breakOn("aa.scrollTop", 2, 0) break when aa.scrollTop is expected(0) 5 | * breakOn("aa.scrollTop", 0) remove the breakpoint 6 | * 7 | */ 8 | function breakOn(property, flag, expected) { 9 | var target = property.match(/(\S*)\.(.*)/); 10 | var object = eval(target[1]), property = target[2]; 11 | var hash_magic = "_$_$" + property; 12 | if (!object.hasOwnProperty(hash_magic)) { 13 | object[hash_magic] = object[property]; 14 | } 15 | 16 | // overwrite with accessor 17 | Object.defineProperty(object, property, { 18 | configurable: true, 19 | get: function () { 20 | return object[hash_magic]; 21 | }, 22 | 23 | set: function (value) { 24 | if (flag === 1) { 25 | debugger; // sets breakpoint 26 | } else if (flag === 2 && value === expected) { 27 | debugger; // sets breakpoint 28 | } 29 | object[hash_magic] = value; 30 | } 31 | }); 32 | } 33 | 34 | function stackTrace() { 35 | var err = new Error(); 36 | console.log(new Date().toLocaleString()); 37 | console.log(err.stack.substr(6)); 38 | } 39 | 40 | function time(fn) { 41 | var start = new Date().getTime(); 42 | 43 | fn.call(fn); 44 | console.log(new Date().getTime() - start); 45 | } 46 | -------------------------------------------------------------------------------- /src/content_scripts/common/observer.js: -------------------------------------------------------------------------------- 1 | import { 2 | getVisibleElements, 3 | initSKFunctionListener, 4 | } from './utils.js'; 5 | 6 | import Mode from './mode'; 7 | 8 | function isElementPositionRelative(elm) { 9 | while (elm !== document.body) { 10 | if (getComputedStyle(elm).position === "relative") { 11 | return true; 12 | } 13 | elm = elm.parentElement; 14 | } 15 | return false; 16 | } 17 | 18 | function startScrollNodeObserver(normal) { 19 | var pendingUpdater = undefined, DOMObserver = new MutationObserver(function (mutations) { 20 | var addedNodes = []; 21 | for (var m of mutations) { 22 | for (var n of m.addedNodes) { 23 | if (n.nodeType === Node.ELEMENT_NODE && !n.fromSurfingKeys) { 24 | n.newlyCreated = true; 25 | addedNodes.push(n); 26 | } 27 | } 28 | } 29 | 30 | if (addedNodes.length) { 31 | if (pendingUpdater) { 32 | clearTimeout(pendingUpdater); 33 | pendingUpdater = undefined; 34 | } 35 | pendingUpdater = setTimeout(function() { 36 | var possibleModalElements = getVisibleElements(function(e, v) { 37 | var br = e.getBoundingClientRect(); 38 | if (br.width > 300 && br.height > 300 39 | && br.width <= window.innerWidth && br.height <= window.innerHeight 40 | && br.top >= 0 && br.left >= 0 41 | && Mode.hasScroll(e, 'y', 16) 42 | && isElementPositionRelative(e) 43 | ) { 44 | v.push(e); 45 | } 46 | }); 47 | 48 | if (possibleModalElements.length) { 49 | normal.addScrollableElement(possibleModalElements[0]); 50 | } 51 | }, 200); 52 | } 53 | }); 54 | DOMObserver.isConnected = false; 55 | 56 | initSKFunctionListener("observer", { 57 | turnOn: () => { 58 | if (!DOMObserver.isConnected) { 59 | DOMObserver.observe(document, { childList: true, subtree:true }); 60 | DOMObserver.isConnected = true; 61 | } 62 | }, 63 | turnOff: () => { 64 | if (DOMObserver.isConnected) { 65 | DOMObserver.disconnect(); 66 | DOMObserver.isConnected = false; 67 | } 68 | }, 69 | }); 70 | } 71 | 72 | export default startScrollNodeObserver; 73 | -------------------------------------------------------------------------------- /src/content_scripts/common/runtime.js: -------------------------------------------------------------------------------- 1 | function dispatchSKEvent(type, args, target) { 2 | if (target === undefined) { 3 | target = document; 4 | } 5 | target.dispatchEvent(new CustomEvent(`surfingkeys:${type}`, { 'detail': args })); 6 | } 7 | 8 | /** 9 | * Call background `action` with `args`, the `callback` will be executed with response from background. 10 | * 11 | * @param {string} action a background action to be called. 12 | * @param {object} args the parameters to be passed to the background action. 13 | * @param {function} callback a function to be executed with the result from the background action. 14 | * 15 | * @example 16 | * 17 | * RUNTIME('getTabs', {queryInfo: {currentWindow: true}}, response => { 18 | * console.log(response); 19 | * }); 20 | */ 21 | function RUNTIME(action, args, callback) { 22 | var actionsRepeatBackground = ['closeTab', 'nextTab', 'previousTab', 'moveTab', 'reloadTab', 'setZoom', 'closeTabLeft','closeTabRight', 'focusTabByIndex']; 23 | (args = args || {}).action = action; 24 | if (actionsRepeatBackground.indexOf(action) !== -1) { 25 | // if the action can only be repeated in background, pass repeats to background with args, 26 | // and set RUNTIME.repeats 1, so that it won't be repeated in foreground's _handleMapKey 27 | args.repeats = RUNTIME.repeats; 28 | RUNTIME.repeats = 1; 29 | } 30 | try { 31 | args.needResponse = callback !== undefined; 32 | chrome.runtime.sendMessage(args, callback); 33 | if (action === 'read') { 34 | runtime.on('onTtsEvent', callback); 35 | } 36 | } catch (e) { 37 | dispatchSKEvent("front", ['showPopup', '[runtime exception] ' + e]); 38 | } 39 | } 40 | 41 | const runtime = (function() { 42 | const self = { 43 | conf: { 44 | autoSpeakOnInlineQuery: false, 45 | lastKeys: "", 46 | // local part from settings 47 | blocklistPattern: undefined, 48 | lurkingPattern: undefined, 49 | smartCase: true, 50 | caseSensitive: false, 51 | clickablePat: /(https?:\/\/|thunder:\/\/|magnet:)\S+/ig, 52 | clickableSelector: "", 53 | editableSelector: "div.CodeMirror-scroll,div.ace_content", 54 | cursorAtEndOfInput: true, 55 | defaultLLMProvider: "ollama", 56 | defaultSearchEngine: "g", 57 | defaultVoice: "Daniel", 58 | editableBodyCare: true, 59 | enableAutoFocus: true, 60 | enableEmojiInsertion: false, 61 | experiment: false, 62 | focusFirstCandidate: false, 63 | focusOnSaved: true, 64 | hintAlign: "center", 65 | hintExplicit: false, 66 | hintShiftNonActive: false, 67 | historyMUOrder: true, 68 | language: undefined, 69 | lastQuery: "", 70 | modeAfterYank: "", 71 | nextLinkRegex: /(\b(next)\b)|下页|下一页|后页|下頁|下一頁|後頁|>>|»/i, 72 | digitForRepeat: true, 73 | omnibarMaxResults: 10, 74 | omnibarHistoryCacheSize: 100, 75 | omnibarPosition: "middle", 76 | omnibarSuggestion: true, 77 | omnibarSuggestionTimeout: 200, 78 | omnibarTabsQuery: {}, 79 | pageUrlRegex: [], 80 | prevLinkRegex: /(\b(prev|previous)\b)|上页|上一页|前页|上頁|上一頁|前頁|<<|«/i, 81 | richHintsForKeystroke: 1000, 82 | scrollStepSize: 70, 83 | showModeStatus: false, 84 | showProxyInStatusBar: false, 85 | smartPageBoundary: false, 86 | smoothScroll: true, 87 | startToShowEmoji: 2, 88 | stealFocusOnLoad: true, 89 | tabIndicesSeparator: "|", 90 | tabsThreshold: 100, 91 | verticalTabs: true, 92 | textAnchorPat: /(^[\n\r\s]*\S{3,}|\b\S{4,})/g, 93 | ignoredFrameHosts: ["https://tpc.googlesyndication.com"], 94 | scrollFriction: 0, 95 | aceKeybindings: "vim", 96 | caretViewport: null, 97 | mouseSelectToQuery: [], 98 | useNeovim: false, 99 | useLocalMarkdownAPI: true 100 | }, 101 | }, _handlers = {}; 102 | 103 | const getTopURLPromise = new Promise(function(resolve, reject) { 104 | if (window === top) { 105 | resolve(window.location.href); 106 | } else { 107 | RUNTIME("getTopURL", null, function(rs) { 108 | resolve(rs.url); 109 | }); 110 | } 111 | }); 112 | 113 | self.on = function(message, cb) { 114 | _handlers[message] = cb; 115 | }; 116 | self.bookMessage = function(message, cb) { 117 | if (_handlers[message]) { 118 | return false; 119 | } else { 120 | _handlers[message] = cb; 121 | return true; 122 | } 123 | }; 124 | self.releaseMessage = function(message) { 125 | delete _handlers[message]; 126 | }; 127 | 128 | chrome.runtime.onMessage.addListener(function(msg, sender, response) { 129 | if (_handlers[msg.subject]) { 130 | _handlers[msg.subject](msg, sender, response); 131 | } 132 | }); 133 | 134 | self.getTopURL = function(cb) { 135 | getTopURLPromise.then(function(url) { 136 | cb(url); 137 | }); 138 | }; 139 | 140 | self.postTopMessage = function(msg) { 141 | getTopURLPromise.then(function(topUrl) { 142 | if (window === top) { 143 | // Firefox use "resource://pdf.js" as window.origin for pdf viewer 144 | topUrl = window.location.origin; 145 | } 146 | if (topUrl === "null" || new URL(topUrl).origin === "file://") { 147 | topUrl = "*"; 148 | } 149 | top.postMessage({surfingkeys_uihost_data: msg}, topUrl); 150 | }); 151 | }; 152 | 153 | self.getCaseSensitive = function(query) { 154 | return self.conf.caseSensitive || (self.conf.smartCase && /[A-Z]/.test(query)); 155 | }; 156 | 157 | return self; 158 | })(); 159 | 160 | export { 161 | RUNTIME, 162 | dispatchSKEvent, 163 | runtime 164 | }; 165 | -------------------------------------------------------------------------------- /src/content_scripts/common/trie.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A simple implementation of Trie by brook hong, for less memory usage and better performance. 3 | * 4 | * Each node has at most two properties, stem or meta. All other properties are expected to be 5 | * one character, taken to be child of the node. 6 | * 7 | */ 8 | function Trie() { 9 | if (arguments.length > 0) { 10 | this.stem = arguments[0]; 11 | } 12 | if (arguments.length > 1) { 13 | this.meta = arguments[1]; 14 | } 15 | } 16 | 17 | Trie.prototype = { 18 | find: function(word) { 19 | var found = this, len = word.length; 20 | for (var i = 0; i < len && found; i++) { 21 | found = found[word[i]]; 22 | } 23 | return found; 24 | }, 25 | 26 | add: function(word, meta) { 27 | var node = this, len = word.length; 28 | for (var i = 0; i < len; i++) { 29 | var c = word[i]; 30 | if (!node.hasOwnProperty(c)) { 31 | var t = new Trie(c); 32 | node[c] = t; 33 | node = t; 34 | } else { 35 | node = node[c]; 36 | } 37 | } 38 | 39 | meta.word = word; 40 | node.meta = meta; 41 | }, 42 | 43 | remove: function(word) { 44 | var found = this, len = word.length, ancestor = []; 45 | for (var i = 0; i < len && found; i++) { 46 | // keep node in path for later to remove empty nodes 47 | ancestor.push(found); 48 | found = found[word[i]]; 49 | } 50 | if (found) { 51 | var i = ancestor.length - 1, 52 | node = ancestor[i]; 53 | delete node[found.stem]; 54 | var parent = node; 55 | while (parent !== this && Object.keys(parent).length === 1) { 56 | // remove the node if it has only one property -- which should be stem 57 | node = ancestor[--i]; 58 | delete node[parent.stem]; 59 | parent = node; 60 | } 61 | } 62 | return found; 63 | }, 64 | 65 | getWords: function(prefix, withoutStem) { 66 | var ret = [], prefix = (prefix || "") + (withoutStem ? "" : (this.stem || "")); 67 | if (this.hasOwnProperty('meta')) { 68 | ret.push(prefix); 69 | } 70 | for (var k in this) { 71 | if (k.length === 1) { 72 | ret = ret.concat(this[k].getWords(prefix)); 73 | } 74 | } 75 | return ret; 76 | }, 77 | 78 | getMetas: function(criterion) { 79 | var ret = []; 80 | if (this.hasOwnProperty('meta') && criterion(this.meta)) { 81 | ret.push(this.meta); 82 | } 83 | for (var k in this) { 84 | if (k.length === 1) { 85 | ret = ret.concat(this[k].getMetas(criterion)); 86 | } 87 | } 88 | return ret; 89 | }, 90 | 91 | getPrefixWord: function() { 92 | // unmapAllExcept could make this Trie object empty. 93 | if (Object.keys(this).length === 0) { 94 | return ""; 95 | } 96 | var fullWord = "", futureWord = this.stem, node = this; 97 | while (fullWord === "") { 98 | var keys = Object.keys(node); 99 | for (var i = 0; i < keys.length; i++) { 100 | if (keys[i] === 'meta') { 101 | fullWord = node.meta.word; 102 | break; 103 | } else if (keys[i] !== 'stem') { 104 | futureWord = futureWord + keys[i]; 105 | node = node[keys[i]]; 106 | break; 107 | } 108 | } 109 | } 110 | return fullWord.substr(0, fullWord.length - futureWord.length + 1); 111 | } 112 | }; 113 | 114 | export default Trie; 115 | -------------------------------------------------------------------------------- /src/content_scripts/content.css: -------------------------------------------------------------------------------- 1 | div.surfingkeys_hints_host { 2 | display: block; 3 | opacity: 1; 4 | color-scheme: auto; 5 | position: absolute; 6 | inset: 0 0 auto 0; 7 | overflow: visible; 8 | } 9 | div.surfingkeys_match_mark { 10 | background-color: #ff0; 11 | color: #000; 12 | opacity: 0.7; 13 | } 14 | div.surfingkeys_selection_mark { 15 | background-color: #b4d7fe; 16 | color: #000; 17 | opacity: 0.7; 18 | } 19 | div.surfingkeys_cursor { 20 | background-color: #0642CE; 21 | color: #f0f8ff; 22 | } 23 | div.surfingkeys_cursor:empty { 24 | padding: 0px 2px; 25 | min-height: 12px; 26 | } 27 | .sk_cursor_prompt { 28 | z-index: 2147483000; 29 | position: absolute; 30 | display: block; 31 | box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2); 32 | background: white; 33 | } 34 | .sk_cursor_prompt>div>span { 35 | padding-right: 8px; 36 | font-size: 18pt; 37 | vertical-align: middle; 38 | } 39 | .sk_cursor_prompt>div { 40 | border-bottom: 1px solid #eee; 41 | padding: 4px 8px; 42 | } 43 | .sk_cursor_prompt>div.selected { 44 | background: #3174e2; 45 | color: white; 46 | } 47 | #sk_clipboard { 48 | position: fixed; 49 | z-index: -999; 50 | top: 0; 51 | left: 0; 52 | opacity: 0; 53 | } 54 | #sk_frame { 55 | position: fixed; 56 | border: 4px solid #434343; 57 | box-sizing: border-box; 58 | z-index: 2147483000; 59 | background: rgba(202, 202, 202, 0.62); 60 | color: blue; 61 | font-size: 16pt; 62 | font-weight: bold; 63 | display: table-cell; 64 | text-align: center; 65 | vertical-align: middle; 66 | } 67 | -------------------------------------------------------------------------------- /src/content_scripts/firefox.js: -------------------------------------------------------------------------------- 1 | import { start } from './content.js'; 2 | 3 | start(); 4 | -------------------------------------------------------------------------------- /src/content_scripts/gist.js: -------------------------------------------------------------------------------- 1 | var Gist = (function() { 2 | var self = {}; 3 | 4 | function _initGist(token, magic_word, onGistReady) { 5 | httpRequest({ 6 | url: "https://api.github.com/gists", 7 | headers: { 8 | 'Authorization': 'token ' + token 9 | } 10 | }, function(res) { 11 | var gists = JSON.parse(res.text); 12 | var gist = ""; 13 | gists.forEach(function(g) { 14 | if (g.hasOwnProperty('description') && g['description'] === magic_word && g.files.hasOwnProperty(magic_word)) { 15 | gist = g.id; 16 | } 17 | }); 18 | if (gist === "") { 19 | httpRequest({ 20 | url: "https://api.github.com/gists", 21 | headers: { 22 | 'Authorization': 'token ' + token 23 | }, 24 | data: '{ "description": "Surfingkeys", "public": false, "files": { "Surfingkeys": { "content": "Surfingkeys" } } }' 25 | }, function(res) { 26 | var ng = JSON.parse(res.text); 27 | onGistReady(ng.id); 28 | }); 29 | } else { 30 | onGistReady(gist); 31 | } 32 | }); 33 | } 34 | 35 | var _token, _gist = "", _comments = []; 36 | self.initGist = function(token, onGistReady) { 37 | _token = token; 38 | _initGist(_token, "Surfingkeys", function(gist) { 39 | _gist = gist; 40 | onGistReady && onGistReady(gist); 41 | }); 42 | }; 43 | 44 | self.newComment = function(text) { 45 | httpRequest({ 46 | url: "https://api.github.com/gists/{0}/comments".format(_gist), 47 | headers: { 48 | 'Authorization': 'token ' + _token 49 | }, 50 | data: '{"body": "{0}"}'.format(encodeURIComponent(text)) 51 | }, function(res) { 52 | console.log(res); 53 | }); 54 | }; 55 | 56 | function _readComment(cid) { 57 | httpRequest({ 58 | url: "https://api.github.com/gists/{0}/comments/{1}".format(_gist, cid), 59 | headers: { 60 | 'Authorization': 'token ' + _token 61 | } 62 | }, function(res) { 63 | var comment = JSON.parse(res.text); 64 | console.log(decodeURIComponent(comment.body)); 65 | }); 66 | } 67 | self.readComment = function(nr) { 68 | if (nr >= _comments.length) { 69 | httpRequest({ 70 | url: "https://api.github.com/gists/{0}/comments".format(_gist), 71 | headers: { 72 | 'Authorization': 'token ' + _token 73 | } 74 | }, function(res) { 75 | _comments = JSON.parse(res.text).map(function(c) { 76 | return c.id; 77 | }); 78 | if (nr < _comments.length) { 79 | _readComment(_comments[nr]); 80 | } 81 | }); 82 | } else { 83 | _readComment(_comments[nr]); 84 | } 85 | }; 86 | 87 | return self; 88 | })(); 89 | Gist.initGist('****************************************'); 90 | Gist.readComment(1); 91 | // Gist.newComment("abc"); 92 | -------------------------------------------------------------------------------- /src/content_scripts/markdown.js: -------------------------------------------------------------------------------- 1 | import { runtime } from './common/runtime.js'; 2 | import KeyboardUtils from './common/keyboardUtils'; 3 | import { 4 | createElementWithContent, 5 | htmlEncode, 6 | httpRequest, 7 | setSanitizedContent, 8 | showBanner, 9 | } from './common/utils.js'; 10 | import { marked } from 'marked'; 11 | 12 | document.addEventListener("surfingkeys:defaultSettingsLoaded", function(evt) { 13 | const { normal, api } = evt.detail; 14 | const { 15 | mapkey, 16 | Clipboard, 17 | Front, 18 | } = api; 19 | 20 | var desc, content; 21 | 22 | mapkey(';h', '#99Toggle this section', function() { 23 | if (desc.style.display !== "none") { 24 | content.style.height = "100vh"; 25 | desc.style.display = "none"; 26 | } else { 27 | desc.style.display = ""; 28 | content.style.height = (window.innerHeight - desc.offsetHeight) + "px"; 29 | } 30 | }); 31 | 32 | function renderHeaderDescription() { 33 | var words = normal.mappings.getWords().map(function(w) { 34 | var meta = normal.mappings.find(w).meta; 35 | w = KeyboardUtils.decodeKeystroke(w); 36 | if (meta.annotation && meta.annotation.length && meta.feature_group === 99) { 37 | return `
${htmlEncode(w)}${meta.annotation}
`; 38 | } 39 | return null; 40 | }).filter(function(w) { 41 | return w !== null; 42 | }); 43 | 44 | desc = document.querySelector('div.description'); 45 | if (desc) { 46 | desc.remove(); 47 | } 48 | content = document.querySelector('div.content'); 49 | desc = createElementWithContent('div', words.join(""), {class: "description"}); 50 | document.body.insertBefore(desc, content); 51 | content.style.height = (window.innerHeight - desc.offsetHeight) + "px"; 52 | } 53 | 54 | var markdownBody = document.querySelector(".markdown-body"), _source; 55 | 56 | function previewMarkdown(mk) { 57 | _source = mk; 58 | if (runtime.conf.useLocalMarkdownAPI) { 59 | setSanitizedContent(markdownBody, marked.parse(mk)); 60 | } else { 61 | setSanitizedContent(markdownBody, "Loading preview…"); 62 | httpRequest({ 63 | url: "https://api.github.com/markdown/raw", 64 | data: mk 65 | }, function(res) { 66 | setSanitizedContent(markdownBody, res.text); 67 | }); 68 | } 69 | } 70 | 71 | mapkey('sm', '#99Edit markdown source', function() { 72 | Front.showEditor(_source, previewMarkdown, 'markdown'); 73 | }); 74 | 75 | mapkey(';s', '#99Switch markdown parser', function() { 76 | runtime.conf.useLocalMarkdownAPI = !runtime.conf.useLocalMarkdownAPI; 77 | previewMarkdown(_source); 78 | }); 79 | 80 | mapkey('cc', '#99Copy generated html code', function() { 81 | Clipboard.write(markdownBody.innerHTML); 82 | }); 83 | 84 | var mdUrl = window.location.search.substr(3); 85 | 86 | if (mdUrl !== "") { 87 | httpRequest({ 88 | url: mdUrl 89 | }, function(res) { 90 | previewMarkdown(res.text); 91 | }); 92 | } else { 93 | Clipboard.read(function(response) { 94 | previewMarkdown(response.data); 95 | }); 96 | } 97 | 98 | var reader = new FileReader(), inputFile; 99 | reader.onload = function(){ 100 | previewMarkdown(reader.result); 101 | }; 102 | function previewMarkdownFile() { 103 | reader.readAsText(inputFile); 104 | } 105 | var inputFileDiv = document.querySelector("input[type=file]"); 106 | inputFileDiv.onchange = function(evt) { 107 | inputFile = evt.target.files[0]; 108 | previewMarkdownFile(); 109 | }; 110 | 111 | mapkey('of', '#99Open local file.', function() { 112 | inputFileDiv.click(); 113 | }); 114 | 115 | renderHeaderDescription(); 116 | }); 117 | -------------------------------------------------------------------------------- /src/content_scripts/start.js: -------------------------------------------------------------------------------- 1 | import { RUNTIME } from './common/runtime.js'; 2 | import { 3 | setSanitizedContent, 4 | } from './common/utils.js'; 5 | import { marked } from 'marked'; 6 | 7 | RUNTIME("getTopSites", null, function(response) { 8 | var urls = response.urls.map(function(u) { 9 | const favUrl = chrome.runtime.getURL(`/_favicon/?pageUrl=${encodeURIComponent(u.url)}`); 10 | return `
  • ${u.title}
  • `; 11 | }); 12 | setSanitizedContent(document.querySelector("#topSites>ul"), urls.join("\n")); 13 | var source = document.getElementById('quickIntroSource').innerHTML; 14 | setSanitizedContent(document.querySelector('#quickIntro'), marked.parse(source)); 15 | 16 | var screen1 = document.querySelector("#screen1"); 17 | screen1.show(); 18 | screen1.classList.add("fadeIn"); 19 | 20 | var screen2 = document.querySelector("#screen2"); 21 | 22 | document.getElementById('back').onclick = function() { 23 | var cl = screen2.classList; 24 | cl.remove("fadeOut"); 25 | cl.remove("fadeIn"); 26 | cl.add("fadeOut"); 27 | screen2.one('animationend', function() { 28 | screen2.hide(); 29 | screen1.show(); 30 | screen1.classList.add("fadeIn"); 31 | }); 32 | }; 33 | 34 | document.querySelector('#show-full-list-of-surfingkeys>a').onclick = function() { 35 | var cl = screen1.classList; 36 | cl.remove("fadeOut"); 37 | cl.remove("fadeIn"); 38 | cl.add("fadeOut"); 39 | screen1.one('animationend', function() { 40 | screen1.hide(); 41 | screen2.show(); 42 | screen2.classList.add("fadeIn"); 43 | }); 44 | }; 45 | }); 46 | 47 | document.addEventListener("surfingkeys:userSettingsLoaded", function(evt) { 48 | const { getUsage } = evt.detail; 49 | getUsage(function(usage) { 50 | var _usage = document.getElementById('sk_usage'); 51 | setSanitizedContent(_usage, usage); 52 | var keys = Array.from(_usage.querySelectorAll('div')).filter(function(d) { 53 | return d.firstElementChild.matches(".kbd-span"); 54 | }); 55 | var randomTip = document.getElementById("randomTip"); 56 | setInterval(function() { 57 | var i = Math.floor(Math.random()*100000%keys.length); 58 | var cl = randomTip.classList; 59 | cl.remove("fadeOut"); 60 | cl.remove("fadeIn"); 61 | cl.add("fadeOut"); 62 | randomTip.one('animationend', function() { 63 | setSanitizedContent(this, keys[i].innerHTML); 64 | this.classList.add("fadeIn"); 65 | }); 66 | }, 5000); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/content_scripts/ui/command.js: -------------------------------------------------------------------------------- 1 | import { 2 | createElementWithContent, 3 | showBanner, 4 | showPopup, 5 | } from '../common/utils.js'; 6 | import { RUNTIME } from '../common/runtime.js'; 7 | 8 | export default (normal, command, omnibar) => { 9 | command('setProxy', 'setProxy : [proxy_type|PROXY]', function(args) { 10 | // args is an array of arguments 11 | var proxy = ((args.length > 1) ? args[1] : "PROXY") + " " + args[0]; 12 | RUNTIME('updateProxy', { 13 | proxy: proxy 14 | }); 15 | return true; 16 | }); 17 | 18 | command('setProxyMode', 'setProxyMode ', function(args) { 19 | RUNTIME("updateProxy", { 20 | mode: args[0] 21 | }, function(rs) { 22 | if (["byhost", "always"].indexOf(rs.proxyMode) !== -1) { 23 | showBanner("{0}: {1}".format(rs.proxyMode, rs.proxy), 3000); 24 | } else { 25 | showBanner(rs.proxyMode, 3000); 26 | } 27 | }); 28 | // return true to close Omnibar for Commands, false to keep Omnibar on 29 | return true; 30 | }); 31 | 32 | command('listVoices', 'list tts voices', function() { 33 | RUNTIME('getVoices', null, function(response) { 34 | 35 | var voices = response.voices.map(function(s) { 36 | return `${s.voiceName}${s.lang}${s.gender}${s.remote}`; 37 | }); 38 | voices.unshift("voiceNamelanggenderremote"); 39 | showPopup("{0}
    ".format(voices.join(''))); 40 | }); 41 | }); 42 | command('testVoices', 'testVoices ', function(args) { 43 | RUNTIME('getVoices', null, function(response) { 44 | 45 | var voices = response.voices, i = 0; 46 | if (args.length > 0) { 47 | voices = voices.filter(function(v) { 48 | return v.lang.indexOf(args[0]) !== -1; 49 | }); 50 | } 51 | var textToRead = "This is to test voice with SurfingKeys"; 52 | if (args.length > 1) { 53 | textToRead = args[1]; 54 | } 55 | var text; 56 | for (i = 0; i < voices.length - 1; i++) { 57 | text = `${textToRead}, ${voices[i].voiceName} / ${voices[i].lang}.`; 58 | readText(text, { 59 | enqueue: true, 60 | verbose: true, 61 | voiceName: voices[i].voiceName 62 | }); 63 | } 64 | text = `${textToRead}, ${voices[i].voiceName} / ${voices[i].lang}.`; 65 | readText(text, { 66 | enqueue: true, 67 | verbose: true, 68 | voiceName: voices[i].voiceName, 69 | onEnd: function() { 70 | showPopup("All voices test done."); 71 | } 72 | }); 73 | }); 74 | }); 75 | command('stopReading', '#13Stop reading.', function(args) { 76 | RUNTIME('stopReading'); 77 | }); 78 | command('feedkeys', 'feed mapkeys', function(args) { 79 | normal.feedkeys(args[0]); 80 | }); 81 | command('quit', '#5quit chrome', function() { 82 | RUNTIME('quit'); 83 | }); 84 | command('clearHistory', 'clearHistory ', function(args) { 85 | let update = {}; 86 | update[args[0]] = []; 87 | RUNTIME('updateInputHistory', update); 88 | }); 89 | command('listSession', 'list session', function() { 90 | RUNTIME('getSettings', { 91 | key: 'sessions' 92 | }, function(response) { 93 | omnibar.listResults(Object.keys(response.settings.sessions), function(s) { 94 | return createElementWithContent('li', s); 95 | }); 96 | }); 97 | }); 98 | command('createSession', 'createSession [name]', function(args) { 99 | RUNTIME('createSession', { 100 | name: args[0] 101 | }); 102 | }); 103 | command('deleteSession', 'deleteSession [name]', function(args) { 104 | RUNTIME('deleteSession', { 105 | name: args[0] 106 | }); 107 | return true; // to close omnibar after the command executed. 108 | }); 109 | command('openSession', 'openSession [name]', function(args) { 110 | RUNTIME('openSession', { 111 | name: args[0] 112 | }); 113 | }); 114 | command('listQueueURLs', 'list URLs in queue waiting for open', function(args) { 115 | RUNTIME('getQueueURLs', null, function(response) { 116 | omnibar.listResults(response.queueURLs, function(s) { 117 | return createElementWithContent('li', s); 118 | }); 119 | }); 120 | }); 121 | command('clearQueueURLs', 'clear URLs in queue waiting for open', function(args) { 122 | RUNTIME('clearQueueURLs'); 123 | }); 124 | command('timeStamp', 'print time stamp in human readable format', function(args) { 125 | var dt = new Date(parseInt(args[0])); 126 | omnibar.listWords([dt.toString()]); 127 | }); 128 | } 129 | -------------------------------------------------------------------------------- /src/content_scripts/ui/frontend.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 20 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 45 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/content_scripts/uiframe.js: -------------------------------------------------------------------------------- 1 | import { 2 | getBrowserName, 3 | getDocumentOrigin 4 | } from './common/utils.js'; 5 | 6 | function createUiHost(browser, onload) { 7 | var uiHost = document.createElement("div"); 8 | uiHost.style.display = "block"; 9 | uiHost.style.opacity = 1; 10 | uiHost.style.colorScheme = "light"; 11 | var frontEndURL = chrome.runtime.getURL('pages/frontend.html'); 12 | var ifr = document.createElement("iframe"); 13 | ifr.setAttribute('allowtransparency', true); 14 | ifr.setAttribute('frameborder', 0); 15 | ifr.setAttribute('scrolling', "no"); 16 | ifr.setAttribute('class', "sk_ui"); 17 | ifr.setAttribute('src', frontEndURL); 18 | ifr.setAttribute('title', "Surfingkeys"); 19 | ifr.style.position = "fixed"; 20 | ifr.style.left = 0; 21 | ifr.style.bottom = 0; 22 | ifr.style.width = "100%"; 23 | ifr.style.height = 0; 24 | ifr.style.zIndex = 2147483647; 25 | uiHost.attachShadow({ mode: 'open' }); 26 | uiHost.shadowRoot.appendChild(ifr); 27 | 28 | function _onWindowMessage(event) { 29 | var _message = event.data && event.data.surfingkeys_uihost_data; 30 | if (_message === undefined) { 31 | return; 32 | } 33 | if (_message.toFrontend) { 34 | // forward message to frontend 35 | ifr.contentWindow.postMessage({surfingkeys_frontend_data: _message}, frontEndURL); 36 | if (_message.toFrontend && event.source 37 | && ['showStatus', 'showEditor', 'openOmnibar', 'openFinder', 'chooseTab'].indexOf(_message.action) !== -1) { 38 | if (!activeContent || activeContent.window !== event.source) { 39 | // reset active Content 40 | 41 | if (activeContent) { 42 | activeContent.window.postMessage({surfingkeys_content_data: { 43 | action: 'deactivated', 44 | reason: `${_message.action}@${event.timeStamp}` 45 | }}, activeContent.origin); 46 | } 47 | 48 | activeContent = { 49 | window: event.source, 50 | origin: _message.origin 51 | }; 52 | 53 | activeContent.window.postMessage({surfingkeys_content_data: { 54 | action: 'activated', 55 | reason: `${_message.action}@${event.timeStamp}` 56 | }}, activeContent.origin); 57 | } 58 | } 59 | } else if (_message.action && _actions.hasOwnProperty(_message.action)) { 60 | _actions[_message.action](_message); 61 | } else if (_message.toContent) { 62 | // forward message to content 63 | if (activeContent) { 64 | activeContent.window.postMessage({surfingkeys_content_data: _message}, activeContent.origin); 65 | } 66 | } 67 | event.stopImmediatePropagation(); 68 | } 69 | 70 | // top -> frontend: origin 71 | // frontend -> top: 72 | // top -> top: apply user settings 73 | ifr.addEventListener("load", function() { 74 | this.contentWindow.postMessage({surfingkeys_frontend_data: { 75 | action: 'initFrontend', 76 | ack: true, 77 | winSize: [window.innerWidth, window.innerHeight], 78 | origin: getDocumentOrigin() 79 | }}, frontEndURL); 80 | 81 | window.addEventListener('message', _onWindowMessage, true); 82 | 83 | }, {once: true}); 84 | 85 | var lastStateOfPointerEvents = "none", _origOverflowY; 86 | var _actions = {}, activeContent = null; 87 | _actions['initFrontendAck'] = function(response) { 88 | onload(uiHost); 89 | }; 90 | _actions['setFrontFrame'] = function(response) { 91 | ifr.style.height = response.frameHeight; 92 | if (response.pointerEvents) { 93 | ifr.style.pointerEvents = response.pointerEvents; 94 | } 95 | if (response.pointerEvents === "none") { 96 | uiHost.blur(); 97 | ifr.blur(); 98 | // test with https://docs.google.com/ and https://web.whatsapp.com/ 99 | if (lastStateOfPointerEvents !== response.pointerEvents && activeContent) { 100 | if (browser.getBackFocusFromFrontend) { 101 | browser.getBackFocusFromFrontend(); 102 | } else { 103 | activeContent.window.postMessage({surfingkeys_content_data: { 104 | action: 'getBackFocus' 105 | }}, activeContent.origin); 106 | } 107 | } 108 | if (document.body) { 109 | document.body.style.animationFillMode = ""; 110 | document.body.style.overflowY = _origOverflowY; 111 | } 112 | } else { 113 | if (browser.focusFrontend) { 114 | browser.focusFrontend(ifr); 115 | } 116 | if (document.body) { 117 | document.body.style.animationFillMode = "none"; 118 | if (_origOverflowY === undefined) { 119 | _origOverflowY = document.body.style.overflowY; 120 | } 121 | document.body.style.overflowY = 'visible'; 122 | } 123 | } 124 | lastStateOfPointerEvents = response.pointerEvents; 125 | }; 126 | 127 | uiHost.detach = function() { 128 | window.removeEventListener('message', _onWindowMessage, true); 129 | uiHost.remove(); 130 | }; 131 | document.documentElement.appendChild(uiHost); 132 | } 133 | 134 | export default createUiHost; 135 | -------------------------------------------------------------------------------- /src/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/src/icons/128.png -------------------------------------------------------------------------------- /src/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/src/icons/16.png -------------------------------------------------------------------------------- /src/icons/48-l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/src/icons/48-l.png -------------------------------------------------------------------------------- /src/icons/48-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/src/icons/48-x.png -------------------------------------------------------------------------------- /src/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/src/icons/48.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Surfingkeys", 4 | "short_name": "Rich shortcuts in vim spirit for productivity with keyboard.", 5 | "description": "Rich shortcuts to click links/switch tabs/scroll, capture pages, use your browser like vim for productivity.", 6 | "icons": { 7 | "16": "icons/16.png", 8 | "48": "icons/48.png", 9 | "128": "icons/128.png" 10 | }, 11 | "commands": { 12 | "restartext": { 13 | "description": "Restart this extenstion." 14 | }, 15 | "previousTab": { 16 | "description": "Go to the previous tab." 17 | }, 18 | "nextTab": { 19 | "description": "Go to the next tab." 20 | }, 21 | "closeTab": { 22 | "description": "Close the current tab." 23 | }, 24 | "proxyThis": { 25 | "description": "Toggle current site in autoproxy_hosts." 26 | } 27 | }, 28 | "browser_action": { 29 | "default_icon": { 30 | "16": "icons/16.png", 31 | "48": "icons/48.png" 32 | }, 33 | "default_title": "Surfingkeys", 34 | "default_popup": "pages/popup.html" 35 | }, 36 | "author": "brook hong", 37 | "permissions": [ 38 | "nativeMessaging", 39 | "tabs", 40 | "history", 41 | "bookmarks", 42 | "scripting", 43 | "storage", 44 | "sessions", 45 | "downloads", 46 | "topSites", 47 | "clipboardRead", 48 | "clipboardWrite" 49 | ], 50 | "background": { 51 | "scripts": [ 52 | "background.js" 53 | ] 54 | }, 55 | "content_scripts": [ 56 | { 57 | "matches": [ 58 | "" 59 | ], 60 | "match_about_blank": true, 61 | "js": [ 62 | "content.js" 63 | ], 64 | "css": [ 65 | "content.css" 66 | ], 67 | "run_at": "document_start", 68 | "all_frames": true 69 | } 70 | ], 71 | "web_accessible_resources": [ 72 | "pages/neovim.html", 73 | "pages/default.js", 74 | "pages/emoji.tsv", 75 | "pages/l10n.json", 76 | "pages/frontend.html", 77 | "pages/pdf_viewer.html", 78 | "pages/shadow.css", 79 | "pages/default.css" 80 | ], 81 | "content_security_policy": "script-src 'self'; object-src 'self'" 82 | } 83 | -------------------------------------------------------------------------------- /src/nvim/Nvim.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | import type { Transport, MessageType, NvimInterface } from './types'; 4 | import { nvimCommandNames } from './__generated__/constants'; 5 | import WebSocketTransport from './transport/websocket'; 6 | 7 | const NvimEventEmitter = (EventEmitter as unknown) as { new (): NvimInterface }; 8 | 9 | class Nvim extends NvimEventEmitter { 10 | private requestId = 0; 11 | private connectedUrl: String; 12 | 13 | private requestPromises: Record< 14 | string, 15 | { resolve: (result: any) => void; reject: (error: any) => void } > = {}; 16 | 17 | constructor() { 18 | super(); 19 | this.connectedUrl = ""; 20 | this.on('newListener', (eventName: string) => { 21 | if ( 22 | !this.listenerCount(eventName) && 23 | !['close', 'newListener', 'removeListener'].includes(eventName) && 24 | !eventName.startsWith('nvim:') 25 | ) { 26 | this.subscribe(eventName); 27 | } 28 | }); 29 | 30 | this.on('removeListener', (eventName: string) => { 31 | if ( 32 | !this.listenerCount(eventName) && 33 | !['close', 'newListener', 'removeListener'].includes(eventName) && 34 | !eventName.startsWith('nvim:') 35 | ) { 36 | this.unsubscribe(eventName); 37 | } 38 | }); 39 | } 40 | 41 | connect(url: string, onconnected?: () => void): void { 42 | if (this.connectedUrl === url) { 43 | this.emit('nvim:connectExisting'); 44 | if (onconnected) { 45 | onconnected(); 46 | } 47 | return; 48 | } 49 | 50 | const transport = new WebSocketTransport (url); 51 | 52 | transport.on('nvim:data', (params: MessageType) => { 53 | if (params[0] === 0) { 54 | // eslint-disable-next-line no-console 55 | console.error('Unsupported request type', ...params); 56 | } else if (params[0] === 1) { 57 | this.handleResponse(params[1], params[2], params[3]); 58 | } else if (params[0] === 2) { 59 | this.emit(params[1], params[2]); 60 | } 61 | }); 62 | 63 | transport.on('nvim:open', () => { 64 | this.connectedUrl = url; 65 | this.emit('nvim:open'); 66 | if (onconnected) { 67 | onconnected(); 68 | } 69 | }); 70 | transport.on('nvim:close', () => { 71 | this.connectedUrl = ""; 72 | this.emit('nvim:close'); 73 | }); 74 | transport.on('nvim:connection_failed', () => { 75 | this.emit('nvim:connection_failed'); 76 | }); 77 | 78 | (Object.keys(nvimCommandNames) as Array).forEach( 79 | (commandName) => { 80 | (this as any)[commandName] = (...params: any[]) => 81 | this.request(transport, nvimCommandNames[commandName], params); 82 | }, 83 | ); 84 | } 85 | 86 | request(transport: Transport, command: string, params: any[] = []): Promise { 87 | this.requestId += 1; 88 | transport.send('nvim:write', this.requestId, command, params); 89 | return new Promise((resolve, reject) => { 90 | this.requestPromises[this.requestId] = { 91 | resolve, 92 | reject, 93 | }; 94 | }); 95 | } 96 | 97 | private handleResponse(id: number, error: Error, result?: any): void { 98 | if (this.requestPromises[id]) { 99 | if (error) { 100 | this.requestPromises[id].reject(error); 101 | } else { 102 | this.requestPromises[id].resolve(result); 103 | } 104 | delete this.requestPromises[id]; 105 | } 106 | } 107 | 108 | /** 109 | * Fetch current mode from nvim, leaves only first letter to match groups of modes. 110 | * https://neovim.io/doc/user/eval.html#mode() 111 | */ 112 | getShortMode = async (): Promise => { 113 | const { mode } = await this.getMode(); 114 | return mode.replace('CTRL-', '')[0]; 115 | }; 116 | } 117 | 118 | export default Nvim; 119 | -------------------------------------------------------------------------------- /src/nvim/__generated__/constants.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | /** 3 | * Constants generated by `yarn generate-types`. Do not edit manually. 4 | * 5 | * Version: 0.5.0 6 | * Api Level: 7 7 | * Api Compatible: 0 8 | * Api Prerelease: false 9 | */ 10 | 11 | export const nvimCommandNames = { 12 | bufLineCount: 'nvim_buf_line_count', 13 | bufAttach: 'nvim_buf_attach', 14 | bufDetach: 'nvim_buf_detach', 15 | bufGetLines: 'nvim_buf_get_lines', 16 | bufSetLines: 'nvim_buf_set_lines', 17 | bufSetText: 'nvim_buf_set_text', 18 | bufGetOffset: 'nvim_buf_get_offset', 19 | bufGetVar: 'nvim_buf_get_var', 20 | bufGetChangedtick: 'nvim_buf_get_changedtick', 21 | bufGetKeymap: 'nvim_buf_get_keymap', 22 | bufSetKeymap: 'nvim_buf_set_keymap', 23 | bufDelKeymap: 'nvim_buf_del_keymap', 24 | bufGetCommands: 'nvim_buf_get_commands', 25 | bufSetVar: 'nvim_buf_set_var', 26 | bufDelVar: 'nvim_buf_del_var', 27 | bufGetOption: 'nvim_buf_get_option', 28 | bufSetOption: 'nvim_buf_set_option', 29 | bufGetName: 'nvim_buf_get_name', 30 | bufSetName: 'nvim_buf_set_name', 31 | bufIsLoaded: 'nvim_buf_is_loaded', 32 | bufDelete: 'nvim_buf_delete', 33 | bufIsValid: 'nvim_buf_is_valid', 34 | bufGetMark: 'nvim_buf_get_mark', 35 | bufGetExtmarkById: 'nvim_buf_get_extmark_by_id', 36 | bufGetExtmarks: 'nvim_buf_get_extmarks', 37 | bufSetExtmark: 'nvim_buf_set_extmark', 38 | bufDelExtmark: 'nvim_buf_del_extmark', 39 | bufAddHighlight: 'nvim_buf_add_highlight', 40 | bufClearNamespace: 'nvim_buf_clear_namespace', 41 | bufSetVirtualText: 'nvim_buf_set_virtual_text', 42 | bufCall: 'nvim_buf_call', 43 | tabpageListWins: 'nvim_tabpage_list_wins', 44 | tabpageGetVar: 'nvim_tabpage_get_var', 45 | tabpageSetVar: 'nvim_tabpage_set_var', 46 | tabpageDelVar: 'nvim_tabpage_del_var', 47 | tabpageGetWin: 'nvim_tabpage_get_win', 48 | tabpageGetNumber: 'nvim_tabpage_get_number', 49 | tabpageIsValid: 'nvim_tabpage_is_valid', 50 | uiAttach: 'nvim_ui_attach', 51 | uiDetach: 'nvim_ui_detach', 52 | uiTryResize: 'nvim_ui_try_resize', 53 | uiSetOption: 'nvim_ui_set_option', 54 | uiTryResizeGrid: 'nvim_ui_try_resize_grid', 55 | uiPumSetHeight: 'nvim_ui_pum_set_height', 56 | uiPumSetBounds: 'nvim_ui_pum_set_bounds', 57 | exec: 'nvim_exec', 58 | command: 'nvim_command', 59 | getHlByName: 'nvim_get_hl_by_name', 60 | getHlById: 'nvim_get_hl_by_id', 61 | getHlIdByName: 'nvim_get_hl_id_by_name', 62 | setHl: 'nvim_set_hl', 63 | feedkeys: 'nvim_feedkeys', 64 | input: 'nvim_input', 65 | inputMouse: 'nvim_input_mouse', 66 | replaceTermcodes: 'nvim_replace_termcodes', 67 | eval: 'nvim_eval', 68 | execLua: 'nvim_exec_lua', 69 | notify: 'nvim_notify', 70 | callFunction: 'nvim_call_function', 71 | callDictFunction: 'nvim_call_dict_function', 72 | strwidth: 'nvim_strwidth', 73 | listRuntimePaths: 'nvim_list_runtime_paths', 74 | getRuntimeFile: 'nvim_get_runtime_file', 75 | setCurrentDir: 'nvim_set_current_dir', 76 | getCurrentLine: 'nvim_get_current_line', 77 | setCurrentLine: 'nvim_set_current_line', 78 | delCurrentLine: 'nvim_del_current_line', 79 | getVar: 'nvim_get_var', 80 | setVar: 'nvim_set_var', 81 | delVar: 'nvim_del_var', 82 | getVvar: 'nvim_get_vvar', 83 | setVvar: 'nvim_set_vvar', 84 | getOption: 'nvim_get_option', 85 | getAllOptionsInfo: 'nvim_get_all_options_info', 86 | getOptionInfo: 'nvim_get_option_info', 87 | setOption: 'nvim_set_option', 88 | echo: 'nvim_echo', 89 | outWrite: 'nvim_out_write', 90 | errWrite: 'nvim_err_write', 91 | errWriteln: 'nvim_err_writeln', 92 | listBufs: 'nvim_list_bufs', 93 | getCurrentBuf: 'nvim_get_current_buf', 94 | setCurrentBuf: 'nvim_set_current_buf', 95 | listWins: 'nvim_list_wins', 96 | getCurrentWin: 'nvim_get_current_win', 97 | setCurrentWin: 'nvim_set_current_win', 98 | createBuf: 'nvim_create_buf', 99 | openTerm: 'nvim_open_term', 100 | chanSend: 'nvim_chan_send', 101 | openWin: 'nvim_open_win', 102 | listTabpages: 'nvim_list_tabpages', 103 | getCurrentTabpage: 'nvim_get_current_tabpage', 104 | setCurrentTabpage: 'nvim_set_current_tabpage', 105 | createNamespace: 'nvim_create_namespace', 106 | getNamespaces: 'nvim_get_namespaces', 107 | paste: 'nvim_paste', 108 | put: 'nvim_put', 109 | subscribe: 'nvim_subscribe', 110 | unsubscribe: 'nvim_unsubscribe', 111 | getColorByName: 'nvim_get_color_by_name', 112 | getColorMap: 'nvim_get_color_map', 113 | getContext: 'nvim_get_context', 114 | loadContext: 'nvim_load_context', 115 | getMode: 'nvim_get_mode', 116 | getKeymap: 'nvim_get_keymap', 117 | setKeymap: 'nvim_set_keymap', 118 | delKeymap: 'nvim_del_keymap', 119 | getCommands: 'nvim_get_commands', 120 | getApiInfo: 'nvim_get_api_info', 121 | setClientInfo: 'nvim_set_client_info', 122 | getChanInfo: 'nvim_get_chan_info', 123 | listChans: 'nvim_list_chans', 124 | callAtomic: 'nvim_call_atomic', 125 | parseExpression: 'nvim_parse_expression', 126 | listUis: 'nvim_list_uis', 127 | getProcChildren: 'nvim_get_proc_children', 128 | getProc: 'nvim_get_proc', 129 | selectPopupmenuItem: 'nvim_select_popupmenu_item', 130 | setDecorationProvider: 'nvim_set_decoration_provider', 131 | winGetBuf: 'nvim_win_get_buf', 132 | winSetBuf: 'nvim_win_set_buf', 133 | winGetCursor: 'nvim_win_get_cursor', 134 | winSetCursor: 'nvim_win_set_cursor', 135 | winGetHeight: 'nvim_win_get_height', 136 | winSetHeight: 'nvim_win_set_height', 137 | winGetWidth: 'nvim_win_get_width', 138 | winSetWidth: 'nvim_win_set_width', 139 | winGetVar: 'nvim_win_get_var', 140 | winSetVar: 'nvim_win_set_var', 141 | winDelVar: 'nvim_win_del_var', 142 | winGetOption: 'nvim_win_get_option', 143 | winSetOption: 'nvim_win_set_option', 144 | winGetPosition: 'nvim_win_get_position', 145 | winGetTabpage: 'nvim_win_get_tabpage', 146 | winGetNumber: 'nvim_win_get_number', 147 | winIsValid: 'nvim_win_is_valid', 148 | winSetConfig: 'nvim_win_set_config', 149 | winGetConfig: 'nvim_win_get_config', 150 | winHide: 'nvim_win_hide', 151 | winClose: 'nvim_win_close', 152 | winCall: 'nvim_win_call', 153 | } as const; 154 | -------------------------------------------------------------------------------- /src/nvim/features/hideMouseCursor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Hides mouse cursor when you start typing. Shows it again when you move mouse. 3 | */ 4 | function showCursor() { 5 | document.body.style.cursor = 'auto'; 6 | document.addEventListener('keydown', hideCursor); // eslint-disable-line no-use-before-define 7 | document.removeEventListener('mousemove', showCursor); 8 | } 9 | 10 | function hideCursor(): void { 11 | document.body.style.cursor = 'none'; 12 | document.addEventListener('mousemove', showCursor); 13 | document.removeEventListener('keydown', hideCursor); 14 | } 15 | 16 | export default hideCursor; 17 | -------------------------------------------------------------------------------- /src/nvim/input/mouse.ts: -------------------------------------------------------------------------------- 1 | import throttle from 'lodash/throttle'; 2 | 3 | import { modifierPrefix } from './keyboard'; 4 | import { Screen } from '../screen'; 5 | import type Nvim from '../Nvim'; 6 | 7 | const GRID = 0; 8 | 9 | const SCROLL_STEP_X = 6; 10 | const SCROLL_STEP_Y = 3; 11 | const MOUSE_BUTTON = { 12 | 0: 'left', 13 | 1: 'middle', 14 | 2: 'right', 15 | WHEEL: 'wheel', 16 | }; 17 | 18 | const ACTION = { 19 | UP: 'up', 20 | DOWN: 'down', 21 | LEFT: 'left', 22 | RIGHT: 'right', 23 | PRESS: 'press', 24 | DRAG: 'drag', 25 | RELEASE: 'release', 26 | } as const; 27 | 28 | type Action = typeof ACTION[keyof typeof ACTION]; 29 | 30 | type Mouse = { 31 | attach: () => void; 32 | detach: () => void; 33 | }; 34 | 35 | const initMouse = ({ screen, nvim }: { screen: Screen; nvim: Nvim }): Mouse => { 36 | const { screenCoords } = screen; 37 | let scrollDeltaX = 0; 38 | let scrollDeltaY = 0; 39 | 40 | let mouseCoords: [number, number] = [0, 0]; 41 | let mouseButtonDown: boolean; 42 | 43 | const mouseCoordsChanged = (event: MouseEvent) => { 44 | const newCoords = screenCoords(event.clientX, event.clientY); 45 | if (newCoords[0] !== mouseCoords[0] || newCoords[1] !== mouseCoords[1]) { 46 | mouseCoords = newCoords; 47 | return true; 48 | } 49 | return false; 50 | }; 51 | 52 | const buttonName = (event: MouseEvent) => 53 | // @ts-expect-error TODO 54 | event.type === 'wheel' ? MOUSE_BUTTON.WHEEL : MOUSE_BUTTON[event.button]; 55 | 56 | const mouseInput = (event: MouseEvent, action: Action) => { 57 | mouseCoordsChanged(event); 58 | const [col, row] = screenCoords(event.clientX, event.clientY); 59 | const button = buttonName(event); 60 | const modifier = modifierPrefix(event); 61 | nvim.inputMouse(button, action, modifier, GRID, row, col); 62 | }; 63 | 64 | const calculateScroll = (event: MouseEvent) => { 65 | let [scrollX, scrollY] = screenCoords(Math.abs(scrollDeltaX), Math.abs(scrollDeltaY)); 66 | scrollX = Math.floor(scrollX / SCROLL_STEP_X); 67 | scrollY = Math.floor(scrollY / SCROLL_STEP_Y); 68 | 69 | if (scrollY === 0 && scrollX === 0) return; 70 | 71 | if (scrollY !== 0) { 72 | mouseInput(event, scrollDeltaY > 0 ? ACTION.DOWN : ACTION.UP); 73 | scrollDeltaY = 0; 74 | } 75 | 76 | if (scrollX !== 0) { 77 | mouseInput(event, scrollDeltaX > 0 ? ACTION.RIGHT : ACTION.LEFT); 78 | scrollDeltaX = 0; 79 | } 80 | }; 81 | 82 | const handleMousewheel = (event: WheelEvent) => { 83 | const { deltaX, deltaY } = event; 84 | if (scrollDeltaY * deltaY < 0) scrollDeltaY = 0; 85 | scrollDeltaX += deltaX; 86 | scrollDeltaY += deltaY; 87 | calculateScroll(event); 88 | }; 89 | 90 | const handleMousedown = (event: MouseEvent) => { 91 | event.preventDefault(); 92 | event.stopPropagation(); 93 | mouseButtonDown = true; 94 | mouseInput(event, ACTION.PRESS); 95 | }; 96 | 97 | const handleMouseup = (event: MouseEvent) => { 98 | event.preventDefault(); 99 | event.stopPropagation(); 100 | mouseButtonDown = false; 101 | mouseInput(event, ACTION.RELEASE); 102 | }; 103 | 104 | const handleMousemove = (event: MouseEvent) => { 105 | if (mouseButtonDown) { 106 | event.preventDefault(); 107 | event.stopPropagation(); 108 | if (mouseCoordsChanged(event)) mouseInput(event, ACTION.DRAG); 109 | } 110 | }; 111 | 112 | const throttledMousemove = throttle(handleMousemove, 50); 113 | const throttledMousewheel = throttle(handleMousewheel, 10); 114 | 115 | const attach = () => { 116 | nvim.command('set mouse=a'); // Enable mouse events 117 | document.addEventListener('mousedown', handleMousedown); 118 | document.addEventListener('mouseup', handleMouseup); 119 | document.addEventListener('mousemove', throttledMousemove); 120 | document.addEventListener('wheel', throttledMousewheel); 121 | }; 122 | 123 | const detach = () => { 124 | document.removeEventListener('mousedown', handleMousedown); 125 | document.removeEventListener('mouseup', handleMouseup); 126 | document.removeEventListener('mousemove', throttledMousemove); 127 | document.removeEventListener('wheel', throttledMousewheel); 128 | }; 129 | return { 130 | attach , 131 | detach, 132 | } 133 | }; 134 | 135 | export default initMouse; 136 | -------------------------------------------------------------------------------- /src/nvim/lib/getColor.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 3 | import { memoize } from 'lodash'; 4 | 5 | /** 6 | * Get color by number, for example hex number `0xFF0000` becomes `rgb(255,0,0)` 7 | * @param color Color in number 8 | * @param defaultColor Use default color if color is undefined or -1 9 | */ 10 | export const getColor = (color: number | undefined, defaultColor?: string): string | undefined => { 11 | if (typeof color !== 'number' || color === -1) return defaultColor; 12 | return `rgb(${(color >> 16) & 0xff},${(color >> 8) & 0xff},${color & 0xff})`; 13 | }; 14 | 15 | /** 16 | * Get color number from string, for example `rgb(255,0,0)` becomes `0xFF0000` 17 | * @param color Color in rgb string 18 | */ 19 | export const getColorNum = memoize((color?: string): number | undefined => { 20 | if (color) { 21 | const [r, g, b] = color 22 | .replace(/([^0-9,])/g, '') 23 | .split(',') 24 | .map((s) => parseInt(s, 10)); 25 | return (r << 16) + (g << 8) + b; 26 | } 27 | return undefined; 28 | }); 29 | -------------------------------------------------------------------------------- /src/nvim/lib/pixi.ts: -------------------------------------------------------------------------------- 1 | // Customized minimal build for pixi.js 2 | // https://github.com/pixijs/pixi.js/blob/dev/bundles/pixi.js/src/index.ts 3 | 4 | import '@pixi/unsafe-eval' 5 | import { Application } from '@pixi/app'; 6 | import { BatchRenderer, Renderer, Texture } from '@pixi/core'; 7 | import { TickerPlugin } from '@pixi/ticker'; 8 | export { Application, BatchRenderer, Renderer, Texture, TickerPlugin }; 9 | 10 | export { Container } from '@pixi/display'; 11 | export { Graphics } from '@pixi/graphics'; 12 | export { Sprite } from '@pixi/sprite'; 13 | export { extensions } from '@pixi/extensions'; 14 | export { clearTextureCache, TextureCache } from '@pixi/utils'; 15 | -------------------------------------------------------------------------------- /src/nvim/renderer.ts: -------------------------------------------------------------------------------- 1 | import Nvim from './Nvim'; 2 | 3 | import { Settings } from './types'; 4 | 5 | import initScreen from './screen'; 6 | import initKeyboard from './input/keyboard'; 7 | import initMouse from './input/mouse'; 8 | import hideMouseCursor from './features/hideMouseCursor'; 9 | 10 | const getDefaultSettings = (): Settings => ({ 11 | bold: 1, 12 | italic: 1, 13 | underline: 1, 14 | undercurl: 1, 15 | strikethrough: 1, 16 | fontfamily: 'monospace', 17 | fontsize: '12', 18 | lineheight: '1.25', 19 | letterspacing: '0', 20 | }); 21 | 22 | type Renderer = { 23 | nvim: Nvim, 24 | destroy: () => void; 25 | }; 26 | 27 | /** 28 | * Browser renderer 29 | */ 30 | const renderer = (element?: HTMLDivElement): Promise => { 31 | return new Promise((resolve, reject) => { 32 | const nvim = new Nvim(); 33 | const settings = getDefaultSettings(); 34 | settings.element = element; 35 | const screen = initScreen({ nvim, settings }); 36 | const keyboard = initKeyboard({ nvim, screen }); 37 | const mouse = initMouse({ nvim, screen }); 38 | 39 | const attach = () => { 40 | screen.uiAttach(); 41 | keyboard.attach(); 42 | mouse.attach(); 43 | }; 44 | nvim.on('nvim:open', attach); 45 | nvim.on('nvim:connectExisting', attach); 46 | 47 | const destroy = () => { 48 | screen.uiDetach(); 49 | keyboard.detach(); 50 | mouse.detach(); 51 | }; 52 | 53 | hideMouseCursor(); 54 | resolve({nvim, destroy}); 55 | }); 56 | }; 57 | 58 | export default renderer; 59 | export { 60 | getDefaultSettings 61 | }; 62 | -------------------------------------------------------------------------------- /src/nvim/server/NativeMessagingHosts/Surfingkeys.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowed_origins": [ 3 | "chrome-extension://aajlcoiaogpknhgninhopncaldipjdnp/", 4 | "chrome-extension://gfbliohnnapiefjpjlpjnehglfpaknnc/" 5 | ], 6 | "description": "Neovim UI client from Surfingkeys", 7 | "name": "surfingkeys", 8 | "type": "stdio" 9 | } 10 | -------------------------------------------------------------------------------- /src/nvim/server/Surfingkeys.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | 3 | [HKEY_CURRENT_USER\SOFTWARE\Chromium\NativeMessagingHosts\surfingkeys] 4 | @="\\surfingkeys.json" 5 | -------------------------------------------------------------------------------- /src/nvim/server/start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | \nvim.exe --headless -c "luafile \server.lua" 3 | -------------------------------------------------------------------------------- /src/nvim/server/start.sh: -------------------------------------------------------------------------------- 1 | SCRIPT_PATH=$(dirname $BASH_SOURCE[0]) 2 | exec nvim --headless -c "luafile $SCRIPT_PATH/server.lua" 3 | -------------------------------------------------------------------------------- /src/nvim/server/start_none.sh: -------------------------------------------------------------------------------- 1 | SCRIPT_PATH=$(dirname $BASH_SOURCE[0]) 2 | exec nvim --headless -c "luafile $SCRIPT_PATH/server.lua" -u NONE 3 | -------------------------------------------------------------------------------- /src/nvim/transport/websocket.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import type { Transport, Args } from 'src/nvim/types'; 3 | import { encode, decodeMulti } from "@msgpack/msgpack"; 4 | 5 | class WebSocketTransport extends EventEmitter implements Transport { 6 | socket: WebSocket; 7 | buffer: Uint8Array; 8 | open: boolean; 9 | 10 | constructor(url: string) { 11 | super(); 12 | 13 | this.buffer = new Uint8Array(); 14 | this.socket = new WebSocket(`ws://${url}`); 15 | this.open = false; 16 | 17 | this.socket.onmessage = async ({ data }) => { 18 | const newBuf = await data.arrayBuffer(); 19 | const newData = new Uint8Array(newBuf); 20 | const prev = this.buffer; 21 | this.buffer = new Uint8Array(prev.byteLength + newData.byteLength); 22 | this.buffer.set(prev); 23 | this.buffer.set(newData, prev.length); 24 | try { 25 | for (const item of decodeMulti(this.buffer)) { 26 | // console.log('nvim:data', item); 27 | this.emit('nvim:data', item); 28 | } 29 | this.buffer = new Uint8Array(); 30 | } catch (e) { 31 | // this exception is ok, since some packages are too large 32 | // to be received all in one time. 33 | } 34 | }; 35 | this.socket.onopen = () => { 36 | this.open = true; 37 | this.emit('nvim:open'); 38 | }; 39 | this.socket.onclose = () => { 40 | if (this.open) { 41 | this.emit('nvim:close'); 42 | } else { 43 | this.emit('nvim:connection_failed'); 44 | } 45 | }; 46 | } 47 | 48 | send(channel: string, ...args: Args): void { 49 | if (channel === 'nvim:write') { 50 | const req = [0, ...args]; 51 | // console.log(channel, req); 52 | if (this.socket.readyState === WebSocket.OPEN) { 53 | this.socket.send(encode(req)); 54 | } else if (this.open) { 55 | this.open = false; 56 | this.emit('nvim:close'); 57 | } 58 | } 59 | } 60 | 61 | close(): void { 62 | this.socket.close(); 63 | } 64 | } 65 | 66 | export default WebSocketTransport; 67 | -------------------------------------------------------------------------------- /src/nvim/types.ts: -------------------------------------------------------------------------------- 1 | type BooleanSetting = 0 | 1; 2 | 3 | /* eslint-disable camelcase */ 4 | 5 | import type { EventEmitter } from 'events'; 6 | import type TypedEventEmitter from 'strict-event-emitter-types'; 7 | 8 | // Only use relative imports here because https://github.com/microsoft/TypeScript/issues/32999#issuecomment-523558695 9 | // TODO: Bundle .d.ts or something 10 | import type { 11 | UiEvents as UiEventsOriginal, 12 | NvimCommands as NvimCommandsOriginal, 13 | } from './__generated__/types'; 14 | import { nvimCommandNames } from './__generated__/constants'; 15 | 16 | export type RequestMessage = [0, number, string, any[]]; 17 | export type ResponseMessage = [1, number, any, any]; 18 | export type NotificationMessage = [2, string, any[]]; 19 | 20 | export type MessageType = RequestMessage | ResponseMessage | NotificationMessage; 21 | export type ReadCallback = (message: MessageType) => void; 22 | export type OnCloseCallback = () => void; 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | export type Args = any[]; 26 | 27 | export type Listener = (...args: Args) => void; 28 | 29 | /** 30 | * Remote transport between server or main and renderer. 31 | * Use emitter events (`on`, `once` etc) for receiving message, and `send` to send message to other side. 32 | */ 33 | export type Transport = EventEmitter & { 34 | /** 35 | * Send message to remote 36 | */ 37 | send: (channel: string, ...args: Args) => void; 38 | }; 39 | 40 | // Manual refine of the auto-generated UiEvents 41 | // More info: https://neovim.io/doc/user/ui.html 42 | 43 | export type ModeInfo = { 44 | cursor_shape: 'block' | 'horizontal' | 'vertical'; 45 | cell_percentage: number; 46 | blinkwait: number; 47 | blinkon: number; 48 | blinkoff: number; 49 | attr_id: number; 50 | attr_id_lm: number; 51 | short_name: string; // TODO: union 52 | name: string; // TODO: union 53 | mouse_shape: number; 54 | }; 55 | 56 | // TODO: refine this type as a union of `[option, value]` with the correct value type for each option. 57 | export type OptionSet = [ 58 | option: 59 | | 'arabicshape' 60 | | 'ambiwidth' 61 | | 'emoji' 62 | | 'guifont' 63 | | 'guifontwide' 64 | | 'linespace' 65 | | 'mousefocus' 66 | | 'pumblend' 67 | | 'showtabline' 68 | | 'termguicolors' 69 | | 'rgb' 70 | | 'ext_cmdline' 71 | | 'ext_popupmenu' 72 | | 'ext_tabline' 73 | | 'ext_wildmenu' 74 | | 'ext_messages' 75 | | 'ext_linegrid' 76 | | 'ext_multigrid' 77 | | 'ext_hlstate' 78 | | 'ext_termcolors', 79 | value: boolean | string, 80 | ]; 81 | 82 | export type HighlightAttrs = { 83 | foreground?: number; 84 | background?: number; 85 | special?: number; 86 | reverse?: boolean; 87 | standout?: boolean; 88 | italic?: boolean; 89 | bold?: boolean; 90 | underline?: boolean; 91 | undercurl?: boolean; 92 | strikethrough?: boolean; 93 | blend?: number; 94 | }; 95 | 96 | export type Cell = [text: string, hl_id?: number, repeat?: number]; 97 | 98 | type UiEventsPatch = { 99 | mode_info_set: [enabled: boolean, cursor_styles: ModeInfo[]]; 100 | option_set: OptionSet; 101 | hl_attr_define: [id: number, rgb_attrs: HighlightAttrs, cterm_attrs: HighlightAttrs, info: []]; 102 | grid_line: [grid: number, row: number, col_start: number, cells: Cell[]]; 103 | }; 104 | 105 | export type UiEvents = Omit & UiEventsPatch; 106 | 107 | export type UiEventsHandlers = { 108 | [Key in keyof UiEvents]: (params: Array) => void; 109 | }; 110 | 111 | type UiEventsArgsByKey = { 112 | [Key in keyof UiEvents]: [Key, ...Array]; 113 | }; 114 | 115 | export type UiEventsArgs = Array; 116 | 117 | export interface NvimEvents { 118 | redraw: (args: UiEventsArgs) => void; 119 | 120 | close: () => void; 121 | 122 | [x: string]: (...args: any[]) => void; 123 | } 124 | 125 | type NvimCommandsPatch = { 126 | nvim_get_mode: () => { mode: string }; 127 | }; 128 | 129 | export type NvimCommands = Omit & NvimCommandsPatch; 130 | 131 | type NvimCommandsMethods = { 132 | [K in keyof typeof nvimCommandNames]: < 133 | Return = ReturnType 134 | >( 135 | ...args: Parameters 136 | ) => Promise; 137 | }; 138 | export type NvimInterface = TypedEventEmitter & NvimCommandsMethods; 139 | 140 | export type Settings = { 141 | element?: HTMLDivElement, 142 | bold: BooleanSetting; 143 | italic: BooleanSetting; 144 | underline: BooleanSetting; 145 | undercurl: BooleanSetting; 146 | strikethrough: BooleanSetting; 147 | fontfamily: string; 148 | fontsize: string; // TODO: number 149 | lineheight: string; // TODO: number 150 | letterspacing: string; // TODO: number 151 | }; 152 | -------------------------------------------------------------------------------- /src/pages/common.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | } 4 | kbd { 5 | display: inline-block; 6 | padding: 3px 5px; 7 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; 8 | line-height: 10px; 9 | color: #555; 10 | vertical-align: middle; 11 | background-color: #fcfcfc; 12 | border: solid 1px #ccc; 13 | border-bottom-color: #bbb; 14 | border-radius: 3px; 15 | box-shadow: inset 0 -1px 0 #bbb; 16 | } 17 | .description { 18 | padding: 10px 10%; 19 | border-bottom: 1px solid #777; 20 | } 21 | .description>div { 22 | padding: 0px 20px; 23 | border-left: 1px solid #e7e7e7; 24 | display: inline-block; 25 | } 26 | .annotation { 27 | padding-left: 12px; 28 | } 29 | .content { 30 | margin: 0px; 31 | padding: 0px 10%; 32 | height: 90vh; /* window.innerHeight * 0.88 */ 33 | overflow: scroll; 34 | background: #777; 35 | } 36 | .content>div { 37 | background: #fff !important; 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/donation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/src/pages/donation.png -------------------------------------------------------------------------------- /src/pages/images/altText_add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/altText_disclaimer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/altText_done.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/altText_spinner.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/pages/images/altText_warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/annotation-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /src/pages/images/annotation-comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /src/pages/images/annotation-help.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 16 | 18 | 21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/pages/images/annotation-insert.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/pages/images/annotation-key.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /src/pages/images/annotation-newparagraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /src/pages/images/annotation-noicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/images/annotation-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 14 | 21 | 28 | 35 | 42 | 43 | -------------------------------------------------------------------------------- /src/pages/images/annotation-paperclip.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/pages/images/annotation-paragraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /src/pages/images/annotation-pushpin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/images/cursor-editorFreeHighlight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/pages/images/cursor-editorFreeText.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/cursor-editorInk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pages/images/cursor-editorTextHighlight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/pages/images/editor-toolbar-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/pages/images/findbarButton-next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/findbarButton-previous.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/gv-toolbarButton-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/loading-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/src/pages/images/loading-icon.gif -------------------------------------------------------------------------------- /src/pages/images/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/images/messageBar_closingButton.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/messageBar_warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-documentProperties.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-firstPage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-handTool.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-lastPage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-rotateCcw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-rotateCw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-scrollHorizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-scrollPage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-scrollVertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-scrollWrapped.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-selectTool.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-spreadEven.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-spreadNone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/secondaryToolbarButton-spreadOdd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-currentOutlineItem.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-editorFreeText.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-editorHighlight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-editorInk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-editorStamp.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-menuArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-openFile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-pageDown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-pageUp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-presentationMode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-print.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-secondaryToolbarToggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-sidebarToggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-viewAttachments.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-viewLayers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-viewOutline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-viewThumbnail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-zoomIn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/toolbarButton-zoomOut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/images/treeitem-collapsed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/images/treeitem-expanded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/markdown.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Markdown preview 6 | 7 | 8 | 9 | 10 | 11 | 12 |
    13 |
    14 | Loading ... 15 |
    16 |
    17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/pages/neovim.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Neovim from SurfingKeys 7 | 8 | 45 | 46 | 47 |
    48 |

    Welcome to use neovim frontend from Surfingkeys, use i or Alt-i to enter Neovim, where you can use Alt-i to leave Neovim.

    49 | 50 | Neovim UI is suspended, you can use shortcuts from Surfingkeys now, such as E R to switch to other tab, or t to open Omnibar. 51 |
    52 |
    53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/pages/neovim.js: -------------------------------------------------------------------------------- 1 | import { RUNTIME } from '../content_scripts/common/runtime.js'; 2 | import { 3 | setSanitizedContent, 4 | } from '../content_scripts/common/utils.js'; 5 | document.addEventListener("surfingkeys:defaultSettingsLoaded", function(evt) { 6 | const { normal, api } = evt.detail; 7 | 8 | const np = new Promise((resolve, reject) => { 9 | import(/* webpackIgnore: true */ './neovim_lib.js').then((nvimlib) => { 10 | nvimlib.default().then(({nvim, destroy}) => { 11 | function rpc(data) { 12 | const [ event, args ] = data; 13 | if (event === "Enter") { 14 | if (args.length) { 15 | normal.feedkeys(args[0]); 16 | } else { 17 | document.body.classList.add("neovim-disabled"); 18 | normal.enter(); 19 | } 20 | } 21 | } 22 | nvim.on('nvim:open', () => { 23 | nvim.input(''); 24 | nvim.on('surfingkeys:rpc', rpc); 25 | }); 26 | nvim.on('nvim:close', () => { 27 | window.close(); 28 | }); 29 | resolve(nvim); 30 | }); 31 | }); 32 | }); 33 | np.then((nvim) => { 34 | RUNTIME('connectNative', {mode: "standalone"}, (resp) => { 35 | if (resp.error) { 36 | setSanitizedContent(document.querySelector('#overlay'), resp.error); 37 | document.body.classList.add("neovim-disabled"); 38 | } else { 39 | normal.exit(); 40 | api.mapkey('', '', function() { 41 | document.body.classList.remove("neovim-disabled"); 42 | normal.exit(); 43 | }); 44 | api.map('i', ''); 45 | nvim.connect(resp.url); 46 | } 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/pages/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 5% 10%; 3 | width: 80%; 4 | } 5 | .ace_editor { 6 | display: inline-block; 7 | } 8 | #mappings_container { 9 | font-size: 0; 10 | width: 100%; 11 | } 12 | textarea { 13 | display: inline-block; 14 | box-sizing: border-box; 15 | padding-left: 5px; 16 | } 17 | #save_container { 18 | float: right; 19 | } 20 | #save_container>a { 21 | padding-right: 8px; 22 | border-right: 1px solid #000; 23 | } 24 | .infoPointer { 25 | cursor: pointer; 26 | padding: 0px 2px; 27 | text-decoration: underline; 28 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542)); 29 | border: solid 1px #C38A22; 30 | border-radius: 2px; 31 | } 32 | .info { 33 | padding: 8px; 34 | margin: 8px; 35 | border: solid 2px #c8c8c8; 36 | border-radius: 8px; 37 | background: #e7e7e7; 38 | } 39 | 40 | span.tip { 41 | padding-left: 60px; 42 | color: #04ad23; 43 | } 44 | div.aphost { 45 | display: inline-block; 46 | width: 200px; 47 | color: blue; 48 | } 49 | div.aphost>span.remove { 50 | cursor: pointer; 51 | } 52 | 53 | #searchAliases>div, #basicMappings>div { 54 | display: inline-block; 55 | padding: 1px 20px; 56 | border-left: 1px solid #ccc; 57 | } 58 | #searchAliases div.remove { 59 | cursor: pointer; 60 | display: inline-block; 61 | vertical-align: middle; 62 | height: 32px; 63 | padding-right: 10px; 64 | } 65 | #searchAliases span.prompt { 66 | font-size: 24px; 67 | } 68 | 69 | span.annotation { 70 | width: 240px; 71 | display: inline-block; 72 | } 73 | kbd { 74 | white-space: nowrap; 75 | display: inline-block; 76 | padding: 3px 5px; 77 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; 78 | line-height: 10px; 79 | vertical-align: middle; 80 | border: solid 1px #ccc; 81 | border-bottom-color: #bbb; 82 | border-radius: 3px; 83 | box-shadow: inset 0 -1px 0 #bbb; 84 | } 85 | .kbd-span { 86 | width: 80px; 87 | display: inline-block; 88 | cursor: pointer; 89 | } 90 | div.pressedKey { 91 | text-align: center; 92 | padding: 50px; 93 | } 94 | div.pressedKey kbd { 95 | font-size: 24pt; 96 | padding: 16px; 97 | } 98 | #keyPicker { 99 | overflow: auto; 100 | position: fixed; 101 | width: 80%; 102 | max-height: 80%; 103 | top: 10%; 104 | left: 10%; 105 | text-align: left; 106 | box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.8); 107 | z-index: 2147483298; 108 | padding: 1rem; 109 | background: white; 110 | } 111 | #resetSettings { 112 | color: blue; 113 | cursor: pointer; 114 | } 115 | span.highlight { 116 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542)); 117 | } 118 | .proxyPair { 119 | border-radius: 8px; 120 | background: #f1f1f1; 121 | padding: 20px; 122 | margin: 8px 0px; 123 | } 124 | div.deleteProxyPair { 125 | float: right; 126 | cursor: pointer; 127 | } 128 | #addProxyPair { 129 | float: right; 130 | } 131 | .proxyPair input, #localPath { 132 | width: 520px; 133 | } 134 | @media only screen and (max-width: 767px) { 135 | body { 136 | margin: 0px 8px; 137 | width: initial; 138 | } 139 | .proxyPair input, #localPath { 140 | width: initial; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/pages/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | 28 | 29 | Disable Surfingkeys 30 | Settings 31 | Help 32 | Report issue 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/pages/popup.js: -------------------------------------------------------------------------------- 1 | String.prototype.format = function() { 2 | var formatted = this; 3 | for (var i = 0; i < arguments.length; i++) { 4 | var regexp = new RegExp('\\{' + i + '\\}', 'gi'); 5 | formatted = formatted.replace(regexp, arguments[i]); 6 | } 7 | return formatted; 8 | }; 9 | 10 | var disableAll = document.getElementById('disableAll'), 11 | version = "Surfingkeys " + chrome.runtime.getManifest().version; 12 | 13 | function RUNTIME(action, args, callback) { 14 | (args = args || {}).action = action; 15 | args.needResponse = callback !== undefined; 16 | chrome.runtime.sendMessage(args, callback); 17 | } 18 | 19 | function updateStatus(blocklist) { 20 | var disabled = blocklist.hasOwnProperty('.*'); 21 | disableAll.textContent = (disabled ? 'Enable ' : 'Disable ') + version; 22 | RUNTIME('setSurfingkeysIcon', { 23 | status: disabled 24 | }); 25 | } 26 | 27 | RUNTIME('getSettings', { 28 | key: 'blocklist' 29 | }, function(response) { 30 | updateStatus(response.settings.blocklist); 31 | }); 32 | 33 | disableAll.addEventListener('click', function() { 34 | RUNTIME('toggleBlocklist', { 35 | domain: ".*" 36 | }, function(response) { 37 | updateStatus(response.blocklist); 38 | }); 39 | }); 40 | 41 | document.getElementById('reportIssue').addEventListener('click', function () { 42 | window.close(); 43 | var description = "%23%23+Error+details%0A%0A{0}%0A%0ASurfingKeys%3A+{1}%0A%0ABrowser%3A+{2}%0A%0AURL%3A+{3}%0A%0A%23%23+Context%0A%0A%2A%2APlease+replace+this+with+a+description+of+how+you+were+using+SurfingKeys.%2A%2A".format(encodeURIComponent(""), chrome.runtime.getManifest().version, encodeURIComponent(navigator.userAgent), encodeURIComponent("")); 44 | window.open("https://github.com/brookhong/Surfingkeys/issues/new?title={0}&body={1}".format(encodeURIComponent(""), description)); 45 | }); 46 | -------------------------------------------------------------------------------- /src/pages/shadow.css: -------------------------------------------------------------------------------- 1 | :host { 2 | all: initial; 3 | color: #fff; 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | font-size: 12px; 6 | } 7 | iframe.sk_ui { 8 | position: fixed; 9 | left: 0; 10 | bottom: 0; 11 | width:100%; 12 | height: 0px; 13 | z-index: 2147483647; 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/start.css: -------------------------------------------------------------------------------- 1 | #content { 2 | font-size: small; 3 | } 4 | #topSites, #quickIntro { 5 | display: table-cell; 6 | padding: 10 5%; 7 | width: 50%; 8 | } 9 | #topSites i { 10 | vertical-align: middle; 11 | margin-right: 4px; 12 | width: 18px; 13 | height: 18px; 14 | display: inline-block; 15 | } 16 | 17 | #back { 18 | font-size: 20pt; 19 | } 20 | #sk_usage>div { 21 | display: inline-block; 22 | vertical-align: top; 23 | } 24 | #sk_usage .kbd-span { 25 | width: 80px; 26 | text-align: right; 27 | display: inline-block; 28 | } 29 | #sk_usage .feature_name { 30 | text-align: center; 31 | font-weight: bold; 32 | padding-bottom: 4px; 33 | } 34 | #sk_usage .feature_name>span { 35 | border-bottom: 2px solid #888; 36 | } 37 | span.annotation { 38 | padding-left: 32px; 39 | line-height: 22px; 40 | } 41 | kbd { 42 | white-space: nowrap; 43 | display: inline-block; 44 | padding: 3px 5px; 45 | vertical-align: middle; 46 | border: solid 1px #ccc; 47 | border-bottom-color: #bbb; 48 | border-radius: 3px; 49 | box-shadow: inset 0 -1px 0 #bbb; 50 | } 51 | 52 | #sk_usage kbd { 53 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; 54 | line-height: 10px; 55 | } 56 | 57 | @keyframes fadeOut { 58 | 0% { 59 | opacity: 1; 60 | } 61 | 62 | 100% { 63 | opacity: 0; 64 | } 65 | } 66 | 67 | .fadeOut { 68 | animation: 0.2s ease-in-out 1 forwards fadeOut; 69 | } 70 | 71 | @keyframes fadeIn { 72 | 0% { 73 | opacity: 0; 74 | } 75 | 76 | 100% { 77 | opacity: 1; 78 | } 79 | } 80 | 81 | .fadeIn { 82 | animation: 0.2s ease-in-out 1 forwards fadeIn; 83 | } 84 | 85 | #randomTip { 86 | padding: 40px; 87 | font-size: 24pt; 88 | text-align: center; 89 | } 90 | 91 | #randomTip span.annotation { 92 | padding-left: 32px; 93 | } 94 | -------------------------------------------------------------------------------- /src/pages/start.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | New Tab 6 | 7 | 8 | 9 | 10 | 11 | 31 |
    32 | 41 | 46 |
    47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/background/start.test.js: -------------------------------------------------------------------------------- 1 | describe('background start', () => { 2 | let start, messageListener; 3 | 4 | beforeAll(async () => { 5 | global.chrome = { 6 | runtime: { 7 | getManifest: () => { 8 | return {manifest_version: 2}; 9 | }, 10 | connectNative: () => { 11 | return { 12 | postMessage: jest.fn(), 13 | onDisconnect: { 14 | addListener: jest.fn() 15 | }, 16 | onMessage: { 17 | addListener: jest.fn() 18 | } 19 | }; 20 | }, 21 | setUninstallURL: jest.fn(), 22 | onMessage: { 23 | addListener: (cb) => { 24 | messageListener = cb; 25 | } 26 | } 27 | }, 28 | windows: { 29 | onFocusChanged: { 30 | addListener: jest.fn() 31 | }, 32 | }, 33 | commands: { 34 | onCommand: { 35 | addListener: jest.fn() 36 | }, 37 | }, 38 | tabs: { 39 | onCreated: { 40 | addListener: jest.fn() 41 | }, 42 | onMoved: { 43 | addListener: jest.fn() 44 | }, 45 | onActivated: { 46 | addListener: jest.fn() 47 | }, 48 | onAttached: { 49 | addListener: jest.fn() 50 | }, 51 | onDetached: { 52 | addListener: jest.fn() 53 | }, 54 | onRemoved: { 55 | addListener: jest.fn() 56 | }, 57 | onUpdated: { 58 | addListener: jest.fn() 59 | } 60 | }, 61 | } 62 | global.DOMRect = jest.fn(); 63 | 64 | start = require('src/background/start.js').start; 65 | window.crypto = { 66 | getRandomValues: jest.fn(), 67 | }; 68 | 69 | start({ 70 | getLatestHistoryItem: (text, maxResults, cb) => { 71 | chrome.history.search({ 72 | startTime: 0, 73 | text, 74 | maxResults 75 | }, function(items) { 76 | cb(items); 77 | }); 78 | }, 79 | _setNewTabUrl: jest.fn(), 80 | _getContainerName: jest.fn(), 81 | loadRawSettings: jest.fn(), 82 | }); 83 | }); 84 | 85 | test('unexpected fakeAction', async () => { 86 | const logSpy = jest.spyOn(console, 'log').mockImplementationOnce(() => { 87 | /* empty */ 88 | }); 89 | messageListener({action: "fakeAction"}, null, null); 90 | expect(logSpy).toHaveBeenCalledWith("[unexpected runtime message] {\"action\":\"fakeAction\"}"); 91 | }); 92 | 93 | test('getHistory', async () => { 94 | const historyItems = [{ 95 | "id": "1", 96 | "url": "https://www.aaa.com/", 97 | "title": "", 98 | "lastVisitTime": 1555206371136, 99 | "visitCount": 1 100 | }]; 101 | chrome.history = { 102 | search: (detail, cb) => { 103 | cb(historyItems); 104 | } 105 | }; 106 | const sendResponse = jest.fn(); 107 | messageListener({action: "getHistory"}, null, sendResponse); 108 | expect(sendResponse).toHaveBeenCalledWith({"history": historyItems}); 109 | }); 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /tests/content_scripts/common/normal.test.js: -------------------------------------------------------------------------------- 1 | describe('normal mode', () => { 2 | let insert, normal, runtime, Mode; 3 | 4 | beforeAll(async () => { 5 | global.chrome = { 6 | runtime: { 7 | sendMessage: jest.fn(), 8 | onMessage: { 9 | addListener: jest.fn() 10 | } 11 | }, 12 | extension: { 13 | getURL: jest.fn() 14 | } 15 | } 16 | global.DOMRect = jest.fn(); 17 | 18 | runtime = require('src/content_scripts/common/runtime.js').runtime; 19 | Mode = require('src/content_scripts/common/mode.js').default; 20 | Mode.init(); 21 | insert = require('src/content_scripts/common/insert.js').default(); 22 | normal = require('src/content_scripts/common/normal.js').default(insert); 23 | }); 24 | 25 | test("normal /", async () => { 26 | normal.enter(); 27 | await new Promise((r) => { 28 | document.addEventListener("surfingkeys:front", function(evt) { 29 | if (evt.detail.length && evt.detail[0] === "openFinder") { 30 | r(evt); 31 | } 32 | }); 33 | document.body.dispatchEvent(new KeyboardEvent('keydown',{'key':'/'})); 34 | }); 35 | }); 36 | 37 | test("normal enter", async () => { 38 | normal.captureElement = jest.fn(); 39 | normal.enter(); 40 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'y'})); 41 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'G'})); 42 | expect(normal.captureElement).toHaveBeenCalledTimes(1); 43 | 44 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'y'})); 45 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'G'})); 46 | expect(normal.captureElement).toHaveBeenCalledTimes(2); 47 | }); 48 | 49 | test("normal once", async () => { 50 | normal.captureElement = jest.fn(); 51 | normal.once(); 52 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'y'})); 53 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'G'})); 54 | expect(normal.captureElement).toHaveBeenCalledTimes(1); 55 | 56 | // the 2nd yG won't trigger action. 57 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'y'})); 58 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'G'})); 59 | expect(normal.captureElement).toHaveBeenCalledTimes(1); 60 | }); 61 | 62 | test("normal mouse up", async () => { 63 | runtime.conf.mouseSelectToQuery = [ "http://localhost" ]; 64 | await new Promise((r) => { 65 | document.addEventListener("surfingkeys:front", function(evt) { 66 | if (evt.detail.length && evt.detail[0] === "querySelectedWord") { 67 | r(evt); 68 | } 69 | }); 70 | document.body.dispatchEvent(new MouseEvent('mouseup', { 71 | bubbles: true, 72 | cancelable: true, 73 | view: window, 74 | button: 0 75 | })); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/content_scripts/markdown.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const html = fs.readFileSync(path.resolve(__dirname, '../../src/pages/markdown.html'), 'utf8'); 4 | import { waitForEvent } from '../utils'; 5 | 6 | describe('markdown viewer', () => { 7 | let dispatchSKEvent, createClipboard, createInsert, createNormal, 8 | createHints, createVisual, createFront, createAPI, createDefaultMappings; 9 | 10 | let normal, clipboard, api, Mode; 11 | 12 | beforeAll(async () => { 13 | const navigator = { userAgent: "Chrome", platform: "Mac" }; 14 | Object.defineProperty(window, 'navigator', { 15 | value: navigator, 16 | writable: true 17 | }); 18 | 19 | global.chrome = { 20 | runtime: { 21 | getURL: jest.fn(), 22 | sendMessage: jest.fn(), 23 | onMessage: { 24 | addListener: jest.fn() 25 | } 26 | }, 27 | storage: { 28 | local: { 29 | get: jest.fn() 30 | } 31 | } 32 | } 33 | global.DOMRect = jest.fn(); 34 | window.focus = jest.fn(); 35 | document.documentElement.innerHTML = html.toString(); 36 | 37 | dispatchSKEvent = require('src/content_scripts/common/runtime.js').dispatchSKEvent; 38 | createClipboard = require('src/content_scripts/common/clipboard.js').default; 39 | Mode = require('src/content_scripts/common/mode.js').default; 40 | createInsert = require('src/content_scripts/common/insert.js').default; 41 | createNormal = require('src/content_scripts/common/normal.js').default; 42 | createHints = require('src/content_scripts/common/hints.js').default; 43 | createVisual = require('src/content_scripts/common/visual.js').default; 44 | createFront = require('src/content_scripts/front.js').default; 45 | createAPI = require('src/content_scripts/common/api.js').default; 46 | createDefaultMappings = require('src/content_scripts/common/default.js').default; 47 | require('src/content_scripts/markdown'); 48 | 49 | Mode.init(); 50 | document.scrollingElement = {}; 51 | clipboard = createClipboard(); 52 | const insert = createInsert(); 53 | normal = createNormal(insert); 54 | normal.enter(); 55 | const hints = createHints(insert, normal); 56 | const visual = createVisual(clipboard, hints); 57 | const front = createFront(insert, normal, hints, visual); 58 | api = createAPI(clipboard, insert, normal, hints, visual, front, {}); 59 | createDefaultMappings(api, clipboard, insert, normal, hints, visual, front); 60 | }); 61 | 62 | test("verify local shortcuts for markdown preview", async () => { 63 | document.execCommand = jest.fn(); 64 | 65 | expect(normal.mappings.find('of')).toBe(undefined); 66 | expect(document.execCommand).toHaveBeenCalledTimes(0); 67 | 68 | await waitForEvent(document, "surfingkeys:defaultSettingsLoaded", () => { 69 | return true; 70 | }, () => { 71 | dispatchSKEvent('defaultSettingsLoaded', {normal, api}); 72 | }); 73 | 74 | expect(normal.mappings.find('of').meta.word).toBe('of'); 75 | expect(document.execCommand).toHaveBeenCalledTimes(1); 76 | }); 77 | 78 | test("render markdown from clipboard", async () => { 79 | jest.spyOn(clipboard, 'read').mockImplementationOnce((onReady) => { 80 | onReady({data: "* [github](https://github.com)\n* [google](https://google.com)"}); 81 | }); 82 | await waitForEvent(document, "surfingkeys:defaultSettingsLoaded", () => { 83 | return true; 84 | }, () => { 85 | dispatchSKEvent('defaultSettingsLoaded', {normal, api}); 86 | }); 87 | const links = document.querySelectorAll("a"); 88 | expect(links.length).toBe(2); 89 | expect(links[0].href).toBe("https://github.com/"); 90 | }); 91 | 92 | test("follow links generated from markdown", async () => { 93 | jest.spyOn(clipboard, 'read').mockImplementationOnce((onReady) => { 94 | onReady({data: "* [github](https://github.com)\n* [google](https://google.com)"}); 95 | }); 96 | await waitForEvent(document, "surfingkeys:defaultSettingsLoaded", () => { 97 | return true; 98 | }, () => { 99 | dispatchSKEvent('defaultSettingsLoaded', {normal, api}); 100 | }); 101 | 102 | const links = document.querySelectorAll("a"); 103 | links.forEach((l, i) => { 104 | l.getBoundingClientRect = jest.fn(() => { 105 | return { width: 100, height: 10, top: 100 * i, left: 0, bottom: 0, right: 0 }; 106 | }); 107 | }); 108 | document.elementFromPoint = jest.fn(() => { 109 | return null; 110 | }); 111 | expect(document.querySelector("div.surfingkeys_hints_host")).toBe(null); 112 | 113 | document.body.dispatchEvent(new KeyboardEvent('keydown', {'key': 'f'})); 114 | const hint_labels = document.querySelector("div.surfingkeys_hints_host").shadowRoot.querySelectorAll("section>div"); 115 | expect(hint_labels.length).toBe(2); 116 | expect(hint_labels[0].label).toBe("A"); 117 | expect(hint_labels[1].label).toBe("S"); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /tests/content_scripts/ui/frontend.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const html = fs.readFileSync(path.resolve(__dirname, '../../../src/content_scripts/ui/frontend.html'), 'utf8'); 4 | 5 | import { waitForEvent } from '../../utils'; 6 | 7 | describe('ui front', () => { 8 | 9 | let Front; 10 | 11 | beforeAll(async () => { 12 | global.chrome = { 13 | runtime: { 14 | onMessage: { 15 | addListener: jest.fn() 16 | }, 17 | getURL: jest.fn() 18 | }, 19 | storage: { 20 | local: { 21 | get: jest.fn() 22 | } 23 | } 24 | } 25 | global.DOMRect = jest.fn(); 26 | document.documentElement.innerHTML = html.toString(); 27 | Front = require('src/content_scripts/ui/frontend'); 28 | 29 | window.focus = jest.fn(); 30 | document.dispatchEvent = jest.fn(); 31 | await waitForEvent(window, "message", (_msg) => { 32 | return _msg.surfingkeys_uihost_data && _msg.surfingkeys_uihost_data.action === "initFrontendAck"; 33 | }, () => { 34 | window.postMessage({surfingkeys_frontend_data: { action: "initFrontend", ack: true, origin: document.location.origin }}, document.location.origin); 35 | }); 36 | }); 37 | 38 | test('show omnibar', async () => { 39 | const elmOmnibarStyle = document.getElementById("sk_omnibar").style; 40 | expect(elmOmnibarStyle).toHaveProperty('display', 'none'); 41 | await waitForEvent(window, "message", (_msg) => { 42 | return _msg.surfingkeys_uihost_data && _msg.surfingkeys_uihost_data.action === "setFrontFrame"; 43 | }, () => { 44 | window.postMessage({surfingkeys_frontend_data: { action: "openOmnibar", type: "SearchEngine", extra: "b" }}, document.location.origin); 45 | }); 46 | expect(elmOmnibarStyle).not.toHaveProperty('display', 'none'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/content_scripts/ui/omnibar.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const html = fs.readFileSync(path.resolve(__dirname, '../../../src/content_scripts/ui/frontend.html'), 'utf8'); 4 | 5 | import KeyboardUtils from 'src/content_scripts/common/keyboardUtils'; 6 | 7 | describe('ui omnibar', () => { 8 | 9 | let Mode, createOmnibar, omnibar, Front; 10 | beforeAll(async () => { 11 | global.chrome = { 12 | runtime: { 13 | sendMessage: jest.fn(), 14 | onMessage: { 15 | addListener: jest.fn() 16 | }, 17 | getURL: jest.fn() 18 | }, 19 | storage: { 20 | local: { 21 | get: jest.fn() 22 | } 23 | } 24 | } 25 | global.DOMRect = jest.fn(); 26 | window.focus = jest.fn(); 27 | window.postMessage({surfingkeys_frontend_data: { action: "initFrontend", origin: document.location.origin }}, document.location.origin); 28 | 29 | document.documentElement.innerHTML = html.toString(); 30 | createOmnibar = require('src/content_scripts/ui/omnibar').default; 31 | Mode = require('src/content_scripts/common/mode').default; 32 | Front = require('src/content_scripts/ui/frontend').default; 33 | 34 | const elmOmnibar = document.querySelector("#sk_omnibar"); 35 | elmOmnibar.innerHTML = ` 36 | 37 |
    38 | 39 | 40 | 41 | 42 | 43 | 44 |
    45 |
    46 |
      47 |
    • 48 |
      🔖 WebAssembly - "Hello World"
      49 |
      https://www.tutorialspoint.com/webassembly/webassembly_hello_world.htm
      50 |
    • 51 |
    • 52 |
      🔖 From JavaScript to WebAssembly in three steps
      53 |
      https://engineering.q42.nl/webassembly/
      54 |
    • 55 |
    • 56 |
      🔥 GitHub
      57 |
      https://github.com/
      58 |
    • 59 |
    60 |
    61 | `; 62 | elmOmnibar.querySelector('#sk_omnibarSearchResult>ul>li.focused').url = "https://www.tutorialspoint.com/webassembly/webassembly_hello_world.htm"; 63 | document.body.appendChild(elmOmnibar); 64 | omnibar = createOmnibar(Front); 65 | }); 66 | 67 | test('edit focus item in omnibar with editor', async () => { 68 | Front.showEditor = jest.fn(); 69 | Mode.handleMapKey.call(omnibar, { 70 | sk_keyName: KeyboardUtils.encodeKeystroke("") 71 | }); 72 | await new Promise((r) => setTimeout(r, 100)); 73 | expect(Front.showEditor).toHaveBeenCalledTimes(1); 74 | }); 75 | 76 | test("toggle Omnibar's position", async () => { 77 | const elmOmnibarClass = document.getElementById("sk_omnibar").classList; 78 | window.postMessage({surfingkeys_frontend_data: { action: "openOmnibar", type: "URLs", extra: "getAllSites" }}, document.location.origin); 79 | await new Promise((r) => setTimeout(r, 100)); 80 | expect(elmOmnibarClass.value).toContain('sk_omnibar_middle'); 81 | Mode.handleMapKey.call(omnibar, { 82 | sk_keyName: KeyboardUtils.encodeKeystroke("") 83 | }); 84 | await new Promise((r) => setTimeout(r, 100)); 85 | expect(elmOmnibarClass.value).toContain('sk_omnibar_bottom'); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/content_scripts/uiframe.test.js: -------------------------------------------------------------------------------- 1 | describe('uiframe.js', () => { 2 | let uiframe; 3 | 4 | beforeAll(async () => { 5 | global.chrome = { 6 | runtime: { 7 | sendMessage: (args, callback) => { 8 | if (args.action === "getSettings") { 9 | callback({settings}); 10 | } 11 | }, 12 | getURL: jest.fn(), 13 | onMessage: { 14 | addListener: jest.fn() 15 | } 16 | }, 17 | extension: { 18 | getURL: jest.fn() 19 | } 20 | } 21 | global.DOMRect = jest.fn(); 22 | 23 | jest.mock('../../src/content_scripts/common/normal.js', () => (insert) => { 24 | mockNormal = { 25 | enter: jest.fn(), 26 | mappings: new MockTrie() 27 | } 28 | mockNormal.mappings.add('e', { 29 | code: mockHalfPageUp 30 | }); 31 | mockNormal.mappings.add('d', { 32 | code: mockHalfPageDown 33 | }); 34 | mockNormal.mappings.add(';x', { 35 | code: mockClosePage 36 | }); 37 | return mockNormal; 38 | }); 39 | uiframe = require('src/content_scripts/uiframe.js'); 40 | }); 41 | 42 | it("", () => { 43 | uiframe.default(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/nvim/Nvim.test.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import Nvim from 'src/nvim/Nvim'; 3 | 4 | import type { Transport } from 'src/nvim/types'; 5 | 6 | const mockTransport: Transport = Object.assign(new EventEmitter(), { 7 | send: jest.fn() 8 | }); 9 | 10 | jest.mock('src/nvim/transport/websocket', (url) => { 11 | return (url) => { 12 | return mockTransport; 13 | }; 14 | }); 15 | 16 | describe('Nvim', () => { 17 | let nvim: Nvim; 18 | 19 | beforeEach(() => { 20 | nvim = new Nvim(); 21 | nvim.connect("ws://mock"); 22 | }); 23 | 24 | describe('notification', () => { 25 | test('send `nvim_subscribe` when you subscribe', () => { 26 | nvim.on('onSomething', () => null); 27 | expect(mockTransport.send).toHaveBeenCalledWith('nvim:write', 1, 'nvim_subscribe', ['onSomething']); 28 | }); 29 | 30 | test('does not subscribe twice on the same event', () => { 31 | nvim.on('onSomething', () => null); 32 | nvim.on('onSomething', () => null); 33 | expect(mockTransport.send).toHaveBeenCalledWith('nvim:write', 1, 'nvim_subscribe', ['onSomething']); 34 | expect(mockTransport.send).toHaveBeenCalledTimes(1); 35 | }); 36 | 37 | test('send `nvim_unsubscribe` when you subscribe', () => { 38 | const listener = () => null; 39 | nvim.on('onSomething', listener); 40 | nvim.removeListener('onSomething', listener); 41 | expect(mockTransport.send).toHaveBeenCalledWith('nvim:write', 2, 'nvim_unsubscribe', ['onSomething']); 42 | }); 43 | 44 | test('does not unsubscribe if you have events with that name', () => { 45 | const listener = () => null; 46 | const anotherListener = () => null; 47 | nvim.on('onSomething', listener); 48 | nvim.on('onSomething', anotherListener); 49 | nvim.removeListener('onSomething', listener); 50 | expect(mockTransport.send).not.toHaveBeenCalledWith('nvim:write', 2, 'nvim_unsubscribe', ['onSomething']); 51 | }); 52 | 53 | test('receives notification for subscription', () => { 54 | const callback = jest.fn(); 55 | nvim.on('onSomething', callback); 56 | mockTransport.emit('nvim:data', [2, 'onSomething', 'params1']); 57 | expect(callback).toHaveBeenCalledWith('params1'); 58 | mockTransport.emit('nvim:data', [2, 'onSomething', 'params2']); 59 | expect(callback).toHaveBeenCalledWith('params2'); 60 | }); 61 | 62 | test('does not receives notifications that are not subscribed', () => { 63 | const callback = jest.fn(); 64 | nvim.on('onSomething', callback); 65 | mockTransport.emit('nvim:data', [2, 'onSomethingElse', 'params1']); 66 | expect(callback).not.toHaveBeenCalled(); 67 | }); 68 | }); 69 | 70 | describe('request message type', () => { 71 | test('receives result of request', async () => { 72 | const errorSpy = jest.spyOn(console, 'error').mockImplementationOnce(() => { 73 | /* empty */ 74 | }); 75 | mockTransport.emit('nvim:data', [0]); 76 | expect(errorSpy).toHaveBeenCalled(); 77 | }); 78 | }); 79 | 80 | describe('predefined commands', () => { 81 | const commands = [ 82 | ['subscribe', 'subscribe'], 83 | ['unsubscribe', 'unsubscribe'], 84 | ['callFunction', 'call_function'], 85 | ['command', 'command'], 86 | ['input', 'input'], 87 | ['inputMouse', 'input_mouse'], 88 | ['getMode', 'get_mode'], 89 | ['uiTryResize', 'ui_try_resize'], 90 | ['uiAttach', 'ui_attach'], 91 | ['getHlByName', 'get_hl_by_name'], 92 | ['paste', 'paste'], 93 | ] as const; 94 | commands.forEach(([command, request]) => { 95 | test(`${command}`, () => { 96 | nvim[command]('param1', 'param2'); 97 | expect(mockTransport.send).toHaveBeenCalledWith('nvim:write', 1, `nvim_${request}`, ['param1', 'param2']); 98 | }); 99 | }); 100 | 101 | test('eval', () => { 102 | nvim.eval('param1'); 103 | expect(mockTransport.send).toHaveBeenCalledWith('nvim:write', 1, `nvim_eval`, ['param1']); 104 | }); 105 | 106 | test('getShortMode returns mode', async () => { 107 | const resultPromise = nvim.getShortMode(); 108 | mockTransport.emit('nvim:data', [1, 1, null, { mode: 'n' }]); 109 | expect(await resultPromise).toBe('n'); 110 | }); 111 | 112 | test('getShortMode cut CTRL- from mode', async () => { 113 | const resultPromise = nvim.getShortMode(); 114 | mockTransport.emit('nvim:data', [1, 1, null, { mode: 'CTRL-n' }]); 115 | expect(await resultPromise).toBe('n'); 116 | }); 117 | }); 118 | 119 | test('emit `close` when transport emits `nvim:close`', () => { 120 | const callback1 = jest.fn(); 121 | const callback2 = jest.fn(); 122 | 123 | nvim.on('nvim:close', callback1); 124 | nvim.on('nvim:close', callback2); 125 | 126 | mockTransport.emit('nvim:close'); 127 | 128 | expect(callback1).toHaveBeenCalled(); 129 | expect(callback2).toHaveBeenCalled(); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /tests/nvim/__image_snapshots__/screen-test-ts-screen-match-snapshot-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/tests/nvim/__image_snapshots__/screen-test-ts-screen-match-snapshot-1-snap.png -------------------------------------------------------------------------------- /tests/nvim/__image_snapshots__/screen-test-ts-screen-redraw-screen-on-default-colors-set-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/tests/nvim/__image_snapshots__/screen-test-ts-screen-redraw-screen-on-default-colors-set-1-snap.png -------------------------------------------------------------------------------- /tests/nvim/__image_snapshots__/screen-test-ts-screen-undercurl-show-undercurl-behind-the-text-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookhong/Surfingkeys/fe19dd5f63507f0e5213af2fd0d048912110b1f4/tests/nvim/__image_snapshots__/screen-test-ts-screen-undercurl-show-undercurl-behind-the-text-1-snap.png -------------------------------------------------------------------------------- /tests/nvim/getColor.test.ts: -------------------------------------------------------------------------------- 1 | import { getColor, getColorNum } from 'src/nvim/lib/getColor'; 2 | 3 | describe('getColor', () => { 4 | test('0 is black', () => { 5 | expect(getColor(0)).toBe('rgb(0,0,0)'); 6 | }); 7 | 8 | test('0xffffff is white', () => { 9 | expect(getColor(0xffffff)).toBe('rgb(255,255,255)'); 10 | }); 11 | 12 | test('0x333333 is gray', () => { 13 | expect(getColor(0x333333)).toBe('rgb(51,51,51)'); 14 | }); 15 | 16 | test('0x003300 is rgb(0,51,0)', () => { 17 | expect(getColor(0x003300)).toBe('rgb(0,51,0)'); 18 | }); 19 | }); 20 | 21 | describe('getColorNum', () => { 22 | test('rgb(0, 0, 0) is 0', () => { 23 | expect(getColorNum('rgb(0,0,0)')).toBe(0); 24 | }); 25 | 26 | test('rgb(255,255,255) is 0xffffff', () => { 27 | expect(getColorNum('rgb(255,255,255)')).toBe(0xffffff); 28 | }); 29 | 30 | test('rgb(51,51,51) is 0x333333', () => { 31 | expect(getColorNum('rgb(51,51,51)')).toBe(0x333333); 32 | }); 33 | 34 | test('rgb(0,51,0) is 0x00ff00', () => { 35 | expect(getColorNum('rgb(0,51,0)')).toBe(0x003300); 36 | }); 37 | 38 | test('returns undefined for undefined param', () => { 39 | expect(getColorNum()).toBeUndefined(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/nvim/renderer.test.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import initRenderer, { getDefaultSettings } from 'src/nvim/renderer'; 3 | 4 | import initScreen from 'src/nvim/screen'; 5 | import initKeyboard from 'src/nvim/input/keyboard'; 6 | import initMouse from 'src/nvim/input/mouse'; 7 | import hideMouseCursor from 'src/nvim/features/hideMouseCursor'; 8 | import type { Args } from 'src/nvim/types'; 9 | 10 | const mockTransport = new EventEmitter(); 11 | 12 | jest.mock('src/nvim/transport/websocket', () => () => mockTransport); 13 | 14 | const mockNvim = new EventEmitter(); 15 | jest.mock('src/nvim/Nvim', () => () => mockNvim); 16 | jest.mock('src/nvim/screen', () => jest.fn(() => 'fakeScreen')); 17 | jest.mock('src/nvim/input/keyboard', () => jest.fn()); 18 | jest.mock('src/nvim/input/mouse', () => jest.fn()); 19 | jest.mock('src/nvim/features/hideMouseCursor', () => jest.fn()); 20 | 21 | describe('renderer', () => { 22 | const settings = getDefaultSettings(); 23 | 24 | beforeEach(() => { 25 | mockTransport.removeAllListeners(); 26 | initRenderer(); 27 | }); 28 | 29 | test('init screen', () => { 30 | expect(initScreen).toHaveBeenCalledWith({ 31 | nvim: mockNvim, 32 | settings, 33 | }); 34 | }); 35 | 36 | test('init keyboard', () => { 37 | expect(initKeyboard).toHaveBeenCalledWith({ 38 | nvim: mockNvim, 39 | screen: 'fakeScreen', 40 | }); 41 | }); 42 | 43 | test('init mouse', () => { 44 | expect(initMouse).toHaveBeenCalledWith({ 45 | nvim: mockNvim, 46 | screen: 'fakeScreen', 47 | }); 48 | }); 49 | 50 | test('init hideMouseCursor', () => { 51 | expect(hideMouseCursor).toHaveBeenCalledWith(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/nvim/screen.ts: -------------------------------------------------------------------------------- 1 | import puppeteer, { Browser, Page } from 'puppeteer'; 2 | import { DIST_DIR } from '../utils'; 3 | 4 | describe('Screen', () => { 5 | jest.setTimeout(30000); 6 | 7 | let browser: Browser; 8 | let page: Page; 9 | 10 | beforeAll(async () => { 11 | browser = await puppeteer.launch({ 12 | headless: false, 13 | slowMo: 10, 14 | args: [ 15 | `--disable-extensions-except=${DIST_DIR}/test`, 16 | `--load-extension=${DIST_DIR}/test`, 17 | `--user-data-dir=${DIST_DIR}/testdata`, 18 | ], 19 | }); 20 | }); 21 | 22 | afterAll(async () => { 23 | await browser.close(); 24 | }); 25 | 26 | beforeEach(async () => { 27 | page = await browser.newPage(); 28 | 29 | await page.setViewport({ 30 | width: 300, 31 | height: 200, 32 | deviceScaleFactor: 2, 33 | }); 34 | 35 | await page.goto(`chrome-extension://aajlcoiaogpknhgninhopncaldipjdnp/pages/neovim.html`); 36 | await page.waitForSelector('input'); 37 | }); 38 | 39 | afterEach(async () => { 40 | await page.close(); 41 | }); 42 | 43 | it('match snapshot', async () => { 44 | await page.keyboard.type('iHello'); 45 | await page.keyboard.press('Escape'); 46 | 47 | const image = await page.screenshot({path: `${DIST_DIR}/testdata/screen-test-ts-screen-match-snapshot-1-snap.png`}); 48 | expect(image).toMatchImageSnapshot(); 49 | }); 50 | 51 | it('redraw screen on default_colors_set', async () => { 52 | await page.keyboard.type(':u0'); 53 | await page.keyboard.press('Enter'); 54 | await page.keyboard.type(':colorscheme desert'); 55 | await page.keyboard.press('Enter'); 56 | 57 | const image = await page.screenshot({path: `${DIST_DIR}/testdata/screen-test-ts-screen-redraw-screen-on-default-colors-set-1-snap.png`}); 58 | expect(image).toMatchImageSnapshot(); 59 | }); 60 | 61 | describe('undercurl', () => { 62 | test('show undercurl behind the text', async () => { 63 | await page.keyboard.type(':u0'); 64 | await page.keyboard.press('Enter'); 65 | await page.keyboard.type(':set filetype=javascript'); 66 | await page.keyboard.press('Enter'); 67 | await page.keyboard.press('Enter'); 68 | await page.keyboard.type(':syntax on'); 69 | await page.keyboard.press('Enter'); 70 | await page.keyboard.type(':hi Comment gui=undercurl guifg=white guisp=red'); 71 | await page.keyboard.press('Enter'); 72 | await page.keyboard.type('i// Hey!'); 73 | 74 | const image = await page.screenshot({path: `${DIST_DIR}/testdata/screen-test-ts-screen-undercurl-show-undercurl-behind-the-text-1-snap.png`}); 75 | expect(image).toMatchImageSnapshot(); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | export const DIST_DIR = path.resolve(__dirname, '../dist/'); 3 | 4 | /* 5 | * wait for an Event of `obj` that matches `messageToWait` and is delivered from `messageDeliver`. 6 | */ 7 | export const waitForEvent = async (obj, evt, messageToWait, messageDeliver) => { 8 | await new Promise((r) => { 9 | const handler = (evt) => { 10 | var _message = evt.data; 11 | if (messageToWait(_message)) { 12 | obj.removeEventListener(evt, handler, true); 13 | r(_message); 14 | } 15 | }; 16 | obj.addEventListener(evt, handler, true); 17 | messageDeliver(); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "types": ["node", "offscreencanvas"], 5 | "allowJs": true, 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "skipLibCheck": true, 11 | "allowSyntheticDefaultImports": true, 12 | "isolatedModules": true, 13 | "baseUrl": "." 14 | }, 15 | "include": ["src", "@types"] 16 | } 17 | --------------------------------------------------------------------------------