mirror of
https://github.com/vale981/tridactyl
synced 2025-03-06 01:51:40 -05:00
Merge branch 'master' of github.com:cmcaine/tridactyl
This commit is contained in:
commit
844feb4edf
23 changed files with 422 additions and 174 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -8612,7 +8612,7 @@
|
|||
}
|
||||
},
|
||||
"web-ext-types": {
|
||||
"version": "github:michael-zapata/web-ext-types#d3dac19b70dde1ea0162340a481d63ffb78e5a96",
|
||||
"version": "github:kelseasy/web-ext-types#30d79e893b7a30d3fbfd8aa0affedaa8dca0d211",
|
||||
"dev": true
|
||||
},
|
||||
"webidl-conversions": {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"uglify-es": "^3.1.5",
|
||||
"uglifyjs-webpack-plugin": "^1.0.0-rc.0",
|
||||
"web-ext": "^1.8.1",
|
||||
"web-ext-types": "github:michael-zapata/web-ext-types",
|
||||
"web-ext-types": "github:kelseasy/web-ext-types",
|
||||
"webpack": "^3.8.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
shopt -s globstar
|
||||
sed -i '/<\/body>/s/^/<script src="..\/..\/..\/content.js"><\/script><link rel="stylesheet" href="..\/..\/content.css"><link rel="stylesheet" href="..\/..\/hint.css">/' $1
|
||||
sed -i '/<\/body>/s@^@<script src="/content.js"></script><link rel="stylesheet" href="/static/content.css"><link rel="stylesheet" href="/static/hint.css">@' $1
|
||||
#static/docs/modules/_excmds_.html
|
||||
|
|
|
@ -38,7 +38,10 @@ class Signature:
|
|||
# Type declaration
|
||||
if ':' in param:
|
||||
name, typ = map(str.strip, param.split(':'))
|
||||
if typ not in ('number', 'boolean', 'string', 'string[]', 'ModeName') and '|' not in typ:
|
||||
if (typ not in ('number', 'boolean', 'string', 'string[]', 'ModeName')
|
||||
and '|' not in typ
|
||||
and typ[0] not in ['"',"'"]
|
||||
):
|
||||
raise Exception("Edit me! " + typ + " is not a supported type!")
|
||||
# Default argument
|
||||
elif '=' in param:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/sh
|
||||
dest=generated/static/docs
|
||||
typedoc --out $dest src --ignoreCompilerErrors
|
||||
./scripts/commandline_injector.sh $dest/modules/_excmds_.html
|
||||
find $dest -name *.html -exec ./scripts/commandline_injector.sh '{}' \;
|
||||
cp -r $dest build/static/
|
||||
|
|
|
@ -5,6 +5,9 @@ const {exec} = require('child_process')
|
|||
function bump_version(versionstr, component = 2) {
|
||||
const versionarr = versionstr.split('.')
|
||||
versionarr[component] = Number(versionarr[component]) + 1
|
||||
for (let smaller = component + 1; smaller <= 2; smaller++) {
|
||||
versionarr[smaller] = 0
|
||||
}
|
||||
return versionarr.join('.')
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import * as Controller from "./controller"
|
||||
import * as keydown_background from "./keydown_background"
|
||||
import * as CommandLine from "./commandline_background"
|
||||
import './lib/browser_proxy_background'
|
||||
|
||||
// Send keys to controller
|
||||
keydown_background.onKeydown.addListener(Controller.acceptKey)
|
||||
|
@ -24,6 +25,7 @@ import * as itertools from './itertools'
|
|||
import * as keyseq from './keyseq'
|
||||
import * as msgsafe from './msgsafe'
|
||||
import * as state from './state'
|
||||
import * as webext from './lib/webext'
|
||||
|
||||
(window as any).tri = Object.assign(Object.create(null), {
|
||||
messaging,
|
||||
|
@ -38,4 +40,6 @@ import * as state from './state'
|
|||
keyseq,
|
||||
msgsafe,
|
||||
state,
|
||||
webext,
|
||||
l: prom => prom.then(console.log).catch(console.error),
|
||||
})
|
||||
|
|
|
@ -101,10 +101,16 @@ function process() {
|
|||
console.log(clInput.value)
|
||||
sendExstr("hidecmdline")
|
||||
sendExstr(clInput.value)
|
||||
if (! browser.extension.inIncognitoContext) {
|
||||
|
||||
// Save non-secret commandlines to the history.
|
||||
const [func,...args] = clInput.value.trim().split(/\s+/)
|
||||
if (! browser.extension.inIncognitoContext &&
|
||||
! (func === 'winopen' && args[0] === '-private')
|
||||
) {
|
||||
state.cmdHistory = state.cmdHistory.concat([clInput.value])
|
||||
}
|
||||
console.log(state.cmdHistory)
|
||||
|
||||
completions.innerHTML = ""
|
||||
clInput.value = ""
|
||||
cmdline_history_position = 0
|
||||
|
|
|
@ -3,9 +3,38 @@
|
|||
// Be careful: typescript elides imports that appear not to be used if they're
|
||||
// assigned to a name. If you want an import just for its side effects, make
|
||||
// sure you import it like this:
|
||||
import "./keydown_content"
|
||||
import "./commandline_content"
|
||||
import "./excmds_content"
|
||||
import "./hinting"
|
||||
/* import "./keydown_content" */
|
||||
/* import "./commandline_content" */
|
||||
/* import "./excmds_content" */
|
||||
/* import "./hinting" */
|
||||
|
||||
console.log("Tridactyl content script loaded, boss!")
|
||||
|
||||
// Add various useful modules to the window for debugging
|
||||
import * as commandline_content from './commandline_content'
|
||||
import * as convert from './convert'
|
||||
import * as dom from './dom'
|
||||
import * as excmds from './excmds_content'
|
||||
import * as hinting_content from './hinting'
|
||||
import * as itertools from './itertools'
|
||||
import * as keydown_content from "./keydown_content"
|
||||
import * as messaging from './messaging'
|
||||
import * as msgsafe from './msgsafe'
|
||||
import state from './state'
|
||||
import * as webext from './lib/webext'
|
||||
|
||||
(window as any).tri = Object.assign(Object.create(null), {
|
||||
browserBg: webext.browserBg,
|
||||
commandline_content,
|
||||
convert,
|
||||
dom,
|
||||
excmds,
|
||||
hinting_content,
|
||||
itertools,
|
||||
keydown_content,
|
||||
messaging,
|
||||
msgsafe,
|
||||
state,
|
||||
webext,
|
||||
l: prom => prom.then(console.log).catch(console.error),
|
||||
})
|
||||
|
|
|
@ -27,7 +27,7 @@ function *ParserController () {
|
|||
let keypress = keyevent.key
|
||||
|
||||
// TODO: think about if this is robust
|
||||
if (state.mode != "ignore"){
|
||||
if (state.mode != "ignore" && state.mode != "hint") {
|
||||
if (isTextEditable(keyevent.target)) {
|
||||
state.mode = "insert"
|
||||
} else if (state.mode === 'insert') {
|
||||
|
@ -40,7 +40,7 @@ function *ParserController () {
|
|||
// yet. So drop them. This also drops all modifier keys.
|
||||
// When we put in handling for other special keys, remember
|
||||
// to continue to ban modifiers.
|
||||
if (state.mode === 'normal' && (keypress.length > 1 || keyevent.ctrlKey || keyevent.altKey)) {
|
||||
if (state.mode === 'normal' && (keypress.length > 1 || keyevent.ctrlKey || keyevent.altKey || keyevent.metaKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
231
src/excmds.ts
231
src/excmds.ts
|
@ -1,5 +1,39 @@
|
|||
// '//#' is a start point for a simple text-replacement-type macro. See excmds_macros.py
|
||||
|
||||
/** # Tridactyl help page
|
||||
|
||||
Use `:help <excmd>` or scroll down to show [[help]] for a particular excmd.
|
||||
|
||||
Tridactyl is in a pretty early stage of development. Please report any issues and make requests for missing features on the GitHub project page [[1]].
|
||||
|
||||
Highlighted features:
|
||||
|
||||
- Press `b` to bring up a list of open tabs in the current window; you can type the tab ID or part of the title or URL to choose a tab (the buffer list doesn't show which one you've selected yet, but it does work)
|
||||
- Press `I` to enter ignore mode. `Shift` + `Escape` to return to normal mode.
|
||||
- Press `f` to start "hint mode", `F` to open in background
|
||||
- Press `o` to `:open` a different page
|
||||
- Press `s` if you want to search for something that looks like a domain name or URL
|
||||
- [[bind]] new commands with e.g. `:bind J tabnext`
|
||||
- Type `:help` to see a list of available excmds
|
||||
- Use `yy` to copy the current page URL to your clipboard
|
||||
- `]]` and `[[` to navigate through the pages of comics, paginated articles, etc
|
||||
- Pressing `ZZ` will close all tabs and windows, but it will only "save" them if your about:preferences are set to "show your tabs and windows from last time"
|
||||
|
||||
There are some caveats common to all webextension vimperator-alikes:
|
||||
|
||||
- Do not try to navigate to any about:\* pages using `:open` as it will fail silently
|
||||
- Firefox will not load Tridactyl on addons.mozilla.org, about:\*, some file:\* URIs, view-source:\*, or data:\*. On these pages Ctrl-L (or F6), Ctrl-Tab and Ctrl-W are your escape hatches
|
||||
- Tridactyl does not currently support changing/hiding the Firefox GUI, but you can do it yourself by changing your userChrome. There is an example file available on our repository [[2]]
|
||||
|
||||
If you want a more fully-featured vimperator-alike, your best option is Firefox ESR [[3]] and Vimperator :)
|
||||
|
||||
[1]: https://github.com/cmcaine/tridactyl/issues
|
||||
[2]: https://github.com/cmcaine/tridactyl/blob/master/src/static/userChrome-minimal.css
|
||||
[3]: https://www.mozilla.org/en-US/firefox/organizations/
|
||||
|
||||
*/
|
||||
/** ignore this line */
|
||||
|
||||
// {{{ setup
|
||||
|
||||
import * as Messaging from "./messaging"
|
||||
|
@ -32,7 +66,6 @@ import {activeTab, activeTabId} from './lib/webext'
|
|||
//#background_helper
|
||||
export const cmd_params = new Map<string, Map<string, string>>()
|
||||
|
||||
/** @hidden */
|
||||
const SEARCH_URLS = new Map<string, string>([
|
||||
["google","https://www.google.com/search?q="],
|
||||
["googleuk","https://www.google.co.uk/search?q="],
|
||||
|
@ -40,32 +73,65 @@ const SEARCH_URLS = new Map<string, string>([
|
|||
["duckduckgo","https://duckduckgo.com/?q="],
|
||||
["yahoo","https://search.yahoo.com/search?p="],
|
||||
["twitter","https://twitter.com/search?q="],
|
||||
["wikipedia","https://en.wikipedia.org/wiki/"],
|
||||
["wikipedia","https://en.wikipedia.org/wiki/Special:Search/"],
|
||||
["youtube","https://www.youtube.com/results?search_query="],
|
||||
["amazon","https://www.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords="],
|
||||
["amazonuk","https://www.amazon.co.uk/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords="],
|
||||
["startpage","https://www.startpage.com/do/search?query="],
|
||||
])
|
||||
|
||||
/** @hidden */
|
||||
function hasScheme(uri: string) {
|
||||
return uri.match(/^(\w+):/)
|
||||
return uri.match(/^([\w-]+):/)
|
||||
}
|
||||
|
||||
/** We use this over encodeURIComponent so that '+'s in non queries are not encoded. */
|
||||
/** @hidden */
|
||||
function searchURL(provider: string, query: string) {
|
||||
if (SEARCH_URLS.has(provider)) {
|
||||
const url = new URL(SEARCH_URLS.get(provider) + query)
|
||||
// URL constructor doesn't convert +s because they're valid literals in
|
||||
// the standard it adheres to. But they are special characters in
|
||||
// x-www-form-urlencoded and e.g. google excepts query parameters in
|
||||
// that format.
|
||||
url.search = url.search.replace(/\+/g, '%2B')
|
||||
return url
|
||||
} else {
|
||||
throw new TypeError(`Unknown provider: '${provider}'`)
|
||||
}
|
||||
}
|
||||
|
||||
/** If maybeURI doesn't have a schema, affix http:// */
|
||||
/** @hidden */
|
||||
function forceURI(maybeURI: string) {
|
||||
if (hasScheme(maybeURI)) {
|
||||
return maybeURI
|
||||
function forceURI(maybeURI: string): string {
|
||||
try {
|
||||
return new URL(maybeURI).href
|
||||
} catch (e) {
|
||||
if (e.name !== 'TypeError') throw e
|
||||
}
|
||||
|
||||
let urlarr = maybeURI.split(" ")
|
||||
if (SEARCH_URLS.get(urlarr[0]) != null){
|
||||
return SEARCH_URLS.get(urlarr[0]) + urlarr.slice(1,urlarr.length).join(" ")
|
||||
} else if (urlarr[0].includes('.')) {
|
||||
return "http://" + maybeURI
|
||||
} else {
|
||||
return SEARCH_URLS.get("google") + maybeURI
|
||||
// Else if search keyword:
|
||||
try {
|
||||
const args = maybeURI.split(' ')
|
||||
return searchURL(args[0], args.slice(1).join(' ')).href
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (e.name !== 'TypeError') throw e
|
||||
}
|
||||
|
||||
// Else if it's a domain or something
|
||||
try {
|
||||
const url = new URL('http://' + maybeURI)
|
||||
// Ignore unlikely domains
|
||||
if (url.hostname.includes('.') || url.port || url.password) {
|
||||
return url.href
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== 'TypeError') throw e
|
||||
}
|
||||
|
||||
// Else search google
|
||||
return searchURL('google', maybeURI).href
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
|
@ -121,11 +187,13 @@ function history(n: number) {
|
|||
window.history.go(n)
|
||||
}
|
||||
|
||||
/** Navigate forward one page in history. */
|
||||
//#content
|
||||
export function forward(n = 1) {
|
||||
history(n)
|
||||
}
|
||||
|
||||
/** Navigate back one page in history. */
|
||||
//#content
|
||||
export function back(n = 1) {
|
||||
history(n * -1)
|
||||
|
@ -145,18 +213,35 @@ export async function reloadhard(n = 1) {
|
|||
reload(n, true)
|
||||
}
|
||||
|
||||
/** Open a new page in the current tab.
|
||||
|
||||
@param urlarr
|
||||
- if first word looks like it has a schema, treat as a URI
|
||||
- else if the first word contains a dot, treat as a domain name
|
||||
- else if the first word is a key of [[SEARCH_URLS]], treat all following terms as search parameters for that provider
|
||||
- else treat as search parameters for google
|
||||
*/
|
||||
//#content
|
||||
export function open(...urlarr: string[]) {
|
||||
let url = urlarr.join(" ")
|
||||
window.location.href = forceURI(url)
|
||||
}
|
||||
|
||||
//#background
|
||||
export function help(...urlarr: string[]) {
|
||||
let url = urlarr.join(" ")
|
||||
// window.location.href = "docs/modules/_excmds_.html#" + url
|
||||
browser.tabs.create({url: "static/docs/modules/_excmds_.html#" + url})
|
||||
/** Show this page.
|
||||
|
||||
`:help <excmd>` jumps to the entry for that command.
|
||||
|
||||
e.g. `:help bind`
|
||||
*/
|
||||
//#background
|
||||
export async function help(excmd?: string) {
|
||||
const docpage = browser.extension.getURL("static/docs/modules/_excmds_.html#")
|
||||
if (excmd === undefined) excmd = "tridactyl-help-page"
|
||||
if ((await activeTab()).url.startsWith(docpage)) {
|
||||
open(docpage + excmd)
|
||||
} else {
|
||||
tabopen(docpage + excmd)
|
||||
}
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
|
@ -167,7 +252,7 @@ function getlinks(){
|
|||
|
||||
/** Find a likely next/previous link and follow it */
|
||||
//#content
|
||||
export function clicknext(dir = "next"){
|
||||
export function clicknext(dir: "next"|"prev" = "next"){
|
||||
let linkarray = Array.from(getlinks())
|
||||
let regarray = [/\bnext|^>$|^(>>|»)$|^(>|»)|(>|»)$|\bmore\b/i, /\bprev\b|\bprevious\b|^<$|^(<<|«)$|^(<|«)|(<|«)$/i]
|
||||
|
||||
|
@ -218,8 +303,7 @@ export function tabprev(increment = 1) {
|
|||
tabnext(increment * -1)
|
||||
}
|
||||
|
||||
// TODO: address should default to some page to which we have access
|
||||
// and focus the location bar
|
||||
/** Like [[open]], but in a new tab */
|
||||
//#background
|
||||
export async function tabopen(...addressarr: string[]) {
|
||||
let uri
|
||||
|
@ -283,6 +367,7 @@ export async function tabmove(n?: string) {
|
|||
browser.tabs.move(aTab.id, {index: m})
|
||||
}
|
||||
|
||||
/** Pin the current tab */
|
||||
//#background
|
||||
export async function pin() {
|
||||
let aTab = await activeTab()
|
||||
|
@ -293,6 +378,7 @@ export async function pin() {
|
|||
|
||||
// {{{ WINDOWS
|
||||
|
||||
/** Like [[open]], but in a new window */
|
||||
//#background
|
||||
export async function winopen(...args: string[]) {
|
||||
let address: string
|
||||
|
@ -311,6 +397,7 @@ export async function winclose() {
|
|||
}
|
||||
|
||||
|
||||
/** Close all windows */
|
||||
// It's unclear if this will leave a session that can be restored.
|
||||
// We might have to do it ourselves.
|
||||
//#background
|
||||
|
@ -323,18 +410,27 @@ export async function qall(){
|
|||
|
||||
// {{{ MISC
|
||||
|
||||
/** Deprecated */
|
||||
//#background
|
||||
export function suppress(preventDefault?: boolean, stopPropagation?: boolean) {
|
||||
keydown.suppress(preventDefault, stopPropagation)
|
||||
mode("ignore")
|
||||
}
|
||||
|
||||
/** Example:
|
||||
- `mode ignore` to ignore all keys.
|
||||
*/
|
||||
//#background
|
||||
export function mode(mode: ModeName) {
|
||||
state.mode = mode
|
||||
// TODO: event emition on mode change.
|
||||
if (mode === "hint") {
|
||||
hint()
|
||||
} else {
|
||||
state.mode = mode
|
||||
}
|
||||
}
|
||||
|
||||
//#background
|
||||
export async function getnexttabs(tabid: number, n?: number) {
|
||||
//#background_helper
|
||||
async function getnexttabs(tabid: number, n?: number) {
|
||||
const curIndex: number = (await browser.tabs.get(tabid)).index
|
||||
const tabs: browser.tabs.Tab[] = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
|
@ -377,6 +473,20 @@ export async function getnexttabs(tabid: number, n?: number) {
|
|||
|
||||
// {{{ CMDLINE
|
||||
|
||||
//#background_helper
|
||||
import * as controller from './controller'
|
||||
|
||||
/** Split `cmds` on pipes (|) and treat each as it's own command.
|
||||
|
||||
Workaround: this should clearly be in the parser, but we haven't come up with a good way to deal with |s in URLs, search terms, etc. yet.
|
||||
*/
|
||||
//#background
|
||||
export function composite(...cmds: string[]) {
|
||||
cmds = cmds.join(" ").split("|")
|
||||
cmds.forEach(controller.acceptExCmd)
|
||||
}
|
||||
|
||||
/** Don't use this */
|
||||
// TODO: These two don't really make sense as excmds, they're internal things.
|
||||
//#content
|
||||
export function showcmdline() {
|
||||
|
@ -384,13 +494,14 @@ export function showcmdline() {
|
|||
CommandLineContent.focus()
|
||||
}
|
||||
|
||||
/** Don't use this */
|
||||
//#content
|
||||
export function hidecmdline() {
|
||||
CommandLineContent.hide()
|
||||
CommandLineContent.blur()
|
||||
}
|
||||
|
||||
/** Set the current value of the commandline to string */
|
||||
/** Set the current value of the commandline to string *with* a trailing space */
|
||||
//#background
|
||||
export function fillcmdline(...strarr: string[]) {
|
||||
let str = strarr.join(" ")
|
||||
|
@ -398,6 +509,7 @@ export function fillcmdline(...strarr: string[]) {
|
|||
messageActiveTab("commandline_frame", "fillcmdline", [str])
|
||||
}
|
||||
|
||||
/** Set the current value of the commandline to string *without* a trailing space */
|
||||
//#background
|
||||
export function fillcmdline_notrail(...strarr: string[]) {
|
||||
let str = strarr.join(" ")
|
||||
|
@ -406,13 +518,27 @@ export function fillcmdline_notrail(...strarr: string[]) {
|
|||
messageActiveTab("commandline_frame", "fillcmdline", [str, trailspace])
|
||||
}
|
||||
|
||||
/** Equivalent to `fillcmdline_notrail <yourargs><current URL>`
|
||||
|
||||
See also [[fillcmdline_notrail]]
|
||||
*/
|
||||
//#background
|
||||
export async function current_url(...strarr: string[]){
|
||||
fillcmdline_notrail(...strarr, (await activeTab()).url)
|
||||
}
|
||||
|
||||
/** Use the system clipboard.
|
||||
|
||||
If `excmd == "open"`, call [[open]] with the contents of the clipboard. Similarly for [[tabopen]].
|
||||
|
||||
If `excmd == "yank"`, copy the current URL, or if given, the value of toYank, into the system clipboard.
|
||||
|
||||
Unfortunately, javascript can only give us the `clipboard` clipboard, not e.g. the X selection clipboard.
|
||||
|
||||
*/
|
||||
//#background
|
||||
export async function clipboard(excmd = "open", content = ""){
|
||||
export async function clipboard(excmd: "open"|"yank"|"tabopen" = "open", ...toYank: string[]) {
|
||||
let content = toYank.join(" ")
|
||||
let url = ""
|
||||
switch (excmd) {
|
||||
case 'yank':
|
||||
|
@ -444,7 +570,7 @@ export async function clipboard(excmd = "open", content = ""){
|
|||
//#background_helper
|
||||
const DEFAULT_FAVICON = browser.extension.getURL("static/defaultFavicon.svg")
|
||||
|
||||
/** Buffer + autocompletions */
|
||||
/** Soon to be deprecated way of showing buffer completions */
|
||||
//#background
|
||||
export async function openbuffer() {
|
||||
fillcmdline("buffer")
|
||||
|
@ -545,24 +671,56 @@ async function listTabs() {
|
|||
|
||||
// {{{ SETTINGS
|
||||
|
||||
/** Bind a sequence of keys to an excmd.
|
||||
|
||||
This is an easier-to-implement bodge while we work on vim-style maps.
|
||||
|
||||
Examples:
|
||||
|
||||
- `bind G fillcmdline tabopen google`
|
||||
- `bind D composite tabclose | tabprev`
|
||||
- `bind j scrollline 20`
|
||||
- `bind F hint -b`
|
||||
|
||||
Use [[composite]] if you want to execute multiple excmds. Use
|
||||
[[fillcmdline]] to put a string in the cmdline and focus the cmdline
|
||||
(otherwise the string is executed immediately).
|
||||
|
||||
See also:
|
||||
|
||||
- [[unbind]]
|
||||
- [[reset]]
|
||||
*/
|
||||
//#background
|
||||
export async function bind(key: string, ...bindarr: string[]){
|
||||
let exstring = bindarr.join(" ")
|
||||
let nmaps = (await browser.storage.sync.get("nmaps"))["nmaps"]
|
||||
nmaps = (nmaps == undefined) ? {} : nmaps
|
||||
nmaps = (nmaps == undefined) ? Object.create(null) : nmaps
|
||||
nmaps[key] = exstring
|
||||
browser.storage.sync.set({nmaps})
|
||||
}
|
||||
|
||||
/** Unbind a sequence of keys so that they do nothing at all.
|
||||
|
||||
See also:
|
||||
|
||||
- [[bind]]
|
||||
- [[reset]]
|
||||
*/
|
||||
//#background
|
||||
export async function unbind(key: string){
|
||||
bind(key)
|
||||
bind(key, "")
|
||||
}
|
||||
|
||||
/* Currently, only resets key to default after extension is reloaded */
|
||||
/** Restores a sequence of keys to their default value.
|
||||
|
||||
See also:
|
||||
|
||||
- [[bind]]
|
||||
- [[unbind]]
|
||||
*/
|
||||
//#background
|
||||
export async function reset(key: string){
|
||||
bind(key)
|
||||
let nmaps = (await browser.storage.sync.get("nmaps"))["nmaps"]
|
||||
nmaps = (nmaps == undefined) ? {} : nmaps
|
||||
delete nmaps[key]
|
||||
|
@ -574,12 +732,13 @@ export async function reset(key: string){
|
|||
// {{{ HINTMODE
|
||||
|
||||
//#background_helper
|
||||
import {hintPageSimple} from './hinting_background'
|
||||
import * as hinting from './hinting_background'
|
||||
|
||||
/** Hint a page. Pass -b as first argument to open hinted page in background. */
|
||||
//#background
|
||||
export function hint() {
|
||||
hintPageSimple()
|
||||
mode('hint')
|
||||
export function hint(option?: "-b") {
|
||||
if (option === '-b') hinting.hintPageOpenInBackground()
|
||||
else hinting.hintPageSimple()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ let modeState: HintState = undefined
|
|||
|
||||
/** For each hintable element, add a hint */
|
||||
export function hintPage(hintableElements: Element[], onSelect: HintSelectedCallback) {
|
||||
state.mode = 'hint'
|
||||
modeState = new HintState()
|
||||
for (let [el, name] of izip(hintableElements, hintnames())) {
|
||||
modeState.hintchars += name
|
||||
|
@ -128,7 +129,6 @@ const HINTCHARS = 'hjklasdfgyuiopqwertnmzxcvb'
|
|||
|
||||
/** Show only hints prefixed by fstr. Focus first match */
|
||||
function filter(fstr) {
|
||||
console.log(fstr)
|
||||
const active: Hint[] = []
|
||||
let foundMatch
|
||||
for (let h of modeState.hints) {
|
||||
|
@ -153,6 +153,7 @@ function filter(fstr) {
|
|||
function reset() {
|
||||
modeState.destructor()
|
||||
modeState = undefined
|
||||
state.mode = 'normal'
|
||||
}
|
||||
|
||||
/** If key is in hintchars, add it to filtstr and filter */
|
||||
|
@ -252,23 +253,61 @@ select,
|
|||
[tabindex]
|
||||
`
|
||||
|
||||
// DEBUGGING
|
||||
/* hintPage(hintables(), hint=>mouseEvent(hint.target, 'click')) */
|
||||
/* addEventListener('keydown', pushKey) */
|
||||
import {activeTab, browserBg, l, firefoxVersionAtLeast} from './lib/webext'
|
||||
|
||||
function hintPageSimple() {
|
||||
console.log("Hinting!")
|
||||
async function openInBackground(url: string) {
|
||||
const thisTab = await activeTab()
|
||||
const options: any = {
|
||||
active: false,
|
||||
url,
|
||||
index: thisTab.index + 1,
|
||||
}
|
||||
if (await l(firefoxVersionAtLeast(57))) options.openerTabId = thisTab.id
|
||||
return browserBg.tabs.create(options)
|
||||
}
|
||||
|
||||
/** if `target === _blank` clicking the link is treated as opening a popup and is blocked. Use webext API to avoid that. */
|
||||
function simulateClick(target: HTMLElement) {
|
||||
// target can be set to other stuff, and we'll fail in annoying ways.
|
||||
// There's no easy way around that while this code executes outside of the
|
||||
// magic 'short lived event handler' context.
|
||||
//
|
||||
// OTOH, hardly anyone uses that functionality any more.
|
||||
if ((target as HTMLAnchorElement).target === '_blank' ||
|
||||
(target as HTMLAnchorElement).target === '_new'
|
||||
) {
|
||||
browserBg.tabs.create({url: (target as HTMLAnchorElement).href})
|
||||
} else {
|
||||
mouseEvent(target, "click")
|
||||
// Sometimes clicking the element doesn't focus it sufficiently.
|
||||
target.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function hintPageOpenInBackground() {
|
||||
hintPage(hintables(), hint=>{
|
||||
hint.target.focus()
|
||||
mouseEvent(hint.target, 'click')
|
||||
if (hint.target.href) {
|
||||
// Try to open with the webext API. If that fails, simulate a click on this page anyway.
|
||||
openInBackground(hint.target.href).catch(()=>simulateClick(hint.target))
|
||||
} else {
|
||||
// This is to mirror vimperator behaviour.
|
||||
simulateClick(hint.target)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hintPageSimple() {
|
||||
hintPage(hintables(), hint=>{
|
||||
simulateClick(hint.target)
|
||||
})
|
||||
}
|
||||
|
||||
function selectFocusedHint() {
|
||||
console.log("Selecting hint.", state.mode)
|
||||
state.mode = 'normal'
|
||||
modeState.focusedHint.select()
|
||||
const focused = modeState.focusedHint
|
||||
reset()
|
||||
focused.select()
|
||||
}
|
||||
|
||||
import {addListener, attributeCaller} from './messaging'
|
||||
|
@ -277,4 +316,5 @@ addListener('hinting_content', attributeCaller({
|
|||
selectFocusedHint,
|
||||
reset,
|
||||
hintPageSimple,
|
||||
hintPageOpenInBackground,
|
||||
}))
|
||||
|
|
|
@ -16,22 +16,27 @@ export async function hintPageSimple() {
|
|||
return await messageActiveTab('hinting_content', 'hintPageSimple')
|
||||
}
|
||||
|
||||
export async function hintPageOpenInBackground() {
|
||||
return await messageActiveTab('hinting_content', 'hintPageOpenInBackground')
|
||||
}
|
||||
|
||||
import {MsgSafeKeyboardEvent} from './msgsafe'
|
||||
|
||||
/** At some point, this might be turned into a real keyseq parser
|
||||
|
||||
if Enter, select focusedHint and reset, or reset on Escape.
|
||||
else give to the hintfilter
|
||||
reset and selectFocusedHints are OK candidates for map targets in the
|
||||
future. pushKey less so, I think.
|
||||
|
||||
*/
|
||||
export function parser(keys: MsgSafeKeyboardEvent[]) {
|
||||
console.log("hintparser", keys)
|
||||
const key = keys[0].key
|
||||
if (key === 'Enter' || key === 'Escape') {
|
||||
if (key === 'Enter') selectFocusedHint()
|
||||
if (key === 'Escape') {
|
||||
reset()
|
||||
return {keys: [], ex_str: 'mode normal'}
|
||||
} else if (['Enter', ' '].includes(key)) {
|
||||
selectFocusedHint()
|
||||
} else {
|
||||
pushKey(keys[0])
|
||||
return {keys: [], ex_str: ''}
|
||||
}
|
||||
return {keys: [], ex_str: ''}
|
||||
}
|
||||
|
|
|
@ -21,21 +21,6 @@ export function recvEvent(event: MsgSafeKeyboardEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
// Sledgehammer keyevent suppression
|
||||
export function suppress(pD?: boolean, sP?: boolean) {
|
||||
if (pD !== undefined) { preventDefault = pD }
|
||||
if (sP !== undefined) { stopPropagation = sP }
|
||||
Messaging.messageAllTabs("keydown_content", "suppress", [preventDefault, stopPropagation])
|
||||
}
|
||||
|
||||
export function state() {
|
||||
return [preventDefault, stopPropagation]
|
||||
}
|
||||
|
||||
// State
|
||||
let preventDefault = false
|
||||
let stopPropagation = false
|
||||
|
||||
// Get messages from content
|
||||
import * as SELF from './keydown_background'
|
||||
Messaging.addListener('keydown_background', Messaging.attributeCaller(SELF))
|
||||
|
|
|
@ -17,87 +17,62 @@ function keyeventHandler(ke: KeyboardEvent) {
|
|||
Messaging.message("keydown_background", "recvEvent", [msgsafe.KeyboardEvent(ke)])
|
||||
}
|
||||
|
||||
|
||||
/** Choose to suppress a key or not based on module state */
|
||||
/** Choose to suppress a key or not */
|
||||
function suppressKey(ke: KeyboardEvent) {
|
||||
// Silly way
|
||||
if (preventDefault) ke.preventDefault()
|
||||
if (stopPropagation) ke.stopPropagation()
|
||||
// Mode specific suppression
|
||||
TerribleModeSpecificSuppression(ke)
|
||||
}
|
||||
|
||||
// Else if in known maps.
|
||||
// StartsWith happens to work for our maps so far. Obviously won't in the future.
|
||||
if ([...nmaps.keys()].find((map) => map.startsWith(ke.key))) {
|
||||
ke.preventDefault()
|
||||
ke.stopPropagation()
|
||||
// {{{ Shitty key suppression workaround.
|
||||
|
||||
import state from './state'
|
||||
|
||||
// Keys not to suppress in normal mode.
|
||||
const normalmodewhitelist = [
|
||||
'/',
|
||||
"'",
|
||||
' ',
|
||||
]
|
||||
|
||||
const hintmodewhitelist = [
|
||||
'F3',
|
||||
'F5',
|
||||
'F12',
|
||||
]
|
||||
|
||||
function TerribleModeSpecificSuppression(ke: KeyboardEvent) {
|
||||
switch (state.mode) {
|
||||
case "normal":
|
||||
// StartsWith happens to work for our maps so far. Obviously won't in the future.
|
||||
/* if (Object.getOwnPropertyNames(nmaps).find((map) => map.startsWith(ke.key))) { */
|
||||
|
||||
if (! ke.ctrlKey && ! ke.metaKey && ! ke.altKey
|
||||
&& ke.key.length === 1
|
||||
&& ! normalmodewhitelist.includes(ke.key)
|
||||
) {
|
||||
ke.preventDefault()
|
||||
ke.stopImmediatePropagation()
|
||||
}
|
||||
break
|
||||
case "hint":
|
||||
if (! hintmodewhitelist.includes(ke.key)) {
|
||||
ke.preventDefault()
|
||||
ke.stopImmediatePropagation()
|
||||
}
|
||||
break;
|
||||
case "ignore":
|
||||
break;
|
||||
case "insert":
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Suppressing all keys from page load means scrolling by arrow key doesn't work until page is reloaded with suppression off. IDK why. Maybe a bug? */
|
||||
export function suppress(pD?: boolean, sP?: boolean) {
|
||||
if (pD !== undefined) { preventDefault = pD }
|
||||
if (sP !== undefined) { stopPropagation = sP }
|
||||
console.log(pD, sP, preventDefault, stopPropagation)
|
||||
}
|
||||
|
||||
/** UNUSED, hardcoded for now */
|
||||
/* export function mapState(maps) { */
|
||||
/* nmaps = maps */
|
||||
/* } */
|
||||
|
||||
// State
|
||||
let preventDefault = false
|
||||
let stopPropagation = false
|
||||
|
||||
/* let nmaps: Map<string, string> */
|
||||
const nmaps = new Map<string, string>([
|
||||
["o", "fillcmdline open"],
|
||||
["O", "current-url open"],
|
||||
["w", "fillcmdline winopen"],
|
||||
["W", "current-url winopen"],
|
||||
["t", "tabopen"],
|
||||
//["t", "fillcmdline tabopen"], // for now, use mozilla completion
|
||||
["]]", "clicknext"],
|
||||
["[[", "clicknext prev"],
|
||||
["T", "current-url tab"],
|
||||
["yy", "clipboard yank"],
|
||||
["p", "clipboard open"],
|
||||
["P", "clipboard tabopen"],
|
||||
["j", "scrollline 10"],
|
||||
["k", "scrollline -10"],
|
||||
["h", "scrollpx -50"],
|
||||
["l", "scrollpx 50"],
|
||||
["G", "scrollto 100"],
|
||||
["gg", "scrollto 0"],
|
||||
["H", "back"],
|
||||
["L", "forward"],
|
||||
["d", "tabclose"],
|
||||
["u", "undo"],
|
||||
["r", "reload"],
|
||||
["R", "reloadhard"],
|
||||
["gt", "tabnext"],
|
||||
["gT", "tabprev"],
|
||||
["gr", "reader"],
|
||||
[":", "fillcmdline"],
|
||||
["s", "fillcmdline google"],
|
||||
["xx", "something"],
|
||||
["i", "insertmode"],
|
||||
["b", "openbuffer"],
|
||||
// Special keys must be prepended with 🄰
|
||||
// ["🄰Backspace", "something"],
|
||||
])
|
||||
// }}}
|
||||
|
||||
// Add listeners
|
||||
window.addEventListener("keydown", keyeventHandler, true)
|
||||
import * as SELF from './keydown_content'
|
||||
Messaging.addListener('keydown_content', Messaging.attributeCaller(SELF))
|
||||
|
||||
// Get current suppression state
|
||||
async function init() {
|
||||
let state = await Messaging.message("keydown_background", "state")
|
||||
suppress(...state)
|
||||
}
|
||||
init()
|
||||
|
||||
|
||||
// Dummy export so that TS treats this as a module.
|
||||
export {}
|
||||
|
|
13
src/lib/browser_proxy.ts
Normal file
13
src/lib/browser_proxy.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {message} from '../messaging'
|
||||
|
||||
const browserProxy = new Proxy(Object.create(null), {
|
||||
get: function(target, api) {
|
||||
return new Proxy({}, {
|
||||
get: function(_, func) {
|
||||
return (...args) => message('browser_proxy_background', 'shim', [api, func, args])
|
||||
}
|
||||
});
|
||||
}
|
||||
}) as typeof browser
|
||||
|
||||
export default browserProxy
|
8
src/lib/browser_proxy_background.ts
Normal file
8
src/lib/browser_proxy_background.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/** Shim to access BG browser APIs from content. */
|
||||
|
||||
function shim(api, func, args) {
|
||||
return browser[api][func](...args)
|
||||
}
|
||||
|
||||
import {addListener, attributeCaller, MessageType} from '../messaging'
|
||||
addListener('browser_proxy_background' as MessageType, attributeCaller({shim}))
|
|
@ -1,3 +1,19 @@
|
|||
import * as convert from '../convert'
|
||||
import browserProxy from './browser_proxy'
|
||||
|
||||
export function inContentScript() {
|
||||
return ! ('tabs' in browser)
|
||||
}
|
||||
|
||||
export let browserBg
|
||||
|
||||
// Make this library work for both content and background.
|
||||
if (inContentScript()) {
|
||||
browserBg = browserProxy
|
||||
} else {
|
||||
browserBg = browser
|
||||
}
|
||||
|
||||
/** await a promise and console.error and rethrow if it errors
|
||||
|
||||
Errors from promises don't get logged unless you seek them out.
|
||||
|
@ -22,10 +38,17 @@ export async function l(promise) {
|
|||
*/
|
||||
//#background_helper
|
||||
export async function activeTab() {
|
||||
return (await l(browser.tabs.query({active: true, currentWindow: true})))[0]
|
||||
return (await l(browserBg.tabs.query({active: true, currentWindow: true})))[0]
|
||||
}
|
||||
|
||||
//#background_helper
|
||||
export async function activeTabId() {
|
||||
return (await activeTab()).id
|
||||
}
|
||||
|
||||
/** Compare major firefox versions */
|
||||
export async function firefoxVersionAtLeast(desiredmajor: number) {
|
||||
const versionstr = (await browserBg.runtime.getBrowserInfo()).version
|
||||
const actualmajor = convert.toNumber(versionstr.split('.')[0])
|
||||
return actualmajor >= desiredmajor
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Tridactyl",
|
||||
"version": "1.0.7",
|
||||
"version": "1.3.1",
|
||||
"icons": {
|
||||
"64": "static/logo/Tridactyl_64px.png",
|
||||
"100": "static/logo/Tridactyl_100px.png",
|
||||
|
@ -56,4 +56,4 @@
|
|||
"strict_min_version": "54.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,8 @@ export type TabMessageType =
|
|||
"hinting_content"
|
||||
export type NonTabMessageType =
|
||||
"keydown_background" |
|
||||
"commandline_background"
|
||||
"commandline_background" |
|
||||
"browser_proxy_background"
|
||||
export type MessageType = TabMessageType | NonTabMessageType
|
||||
|
||||
export interface Message {
|
||||
|
|
|
@ -26,7 +26,7 @@ function convertArgs(params, argv) {
|
|||
for ([type, [i, arg]] of izip(params.values(), enumerate(argv))) {
|
||||
if (type in conversions) {
|
||||
typedArgs.push(conversions[type](arg))
|
||||
} else if (type.includes('|')) {
|
||||
} else if (type.includes('|') || ["'", '"'].includes(type[0])) {
|
||||
// Do your own conversions!
|
||||
typedArgs.push(arg)
|
||||
} else if (type === "string[]") {
|
||||
|
@ -42,7 +42,7 @@ function convertArgs(params, argv) {
|
|||
// TODO: Pipe to separate commands
|
||||
// TODO: Abbreviated commands
|
||||
export function parser(ex_str){
|
||||
let [func,...args] = ex_str.split(" ")
|
||||
let [func,...args] = ex_str.trim().split(/\s+/)
|
||||
if (ExCmds.cmd_params.has(func)) {
|
||||
try {
|
||||
let typedArgs = convertArgs(ExCmds.cmd_params.get(func), args)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
// TODO: stop stealing keys from "insert mode"
|
||||
// r -> refresh page is particularly unhelpful
|
||||
// Can't stringify a map -> just use an object
|
||||
let nmaps = {
|
||||
export const DEFAULTNMAPS = {
|
||||
"o": "fillcmdline open",
|
||||
"O": "current_url open",
|
||||
"w": "fillcmdline winopen",
|
||||
|
@ -44,18 +44,19 @@ let nmaps = {
|
|||
"b": "openbuffer",
|
||||
"ZZ": "qall",
|
||||
"f": "hint",
|
||||
"F": "hint -b",
|
||||
"I": "mode ignore",
|
||||
// Special keys must be prepended with 🄰
|
||||
// ["🄰Backspace", "something"],
|
||||
}
|
||||
|
||||
let nmaps = Object.assign(Object.create(null), DEFAULTNMAPS)
|
||||
|
||||
// Allow config to be changed in settings
|
||||
// TODO: make this more general
|
||||
browser.storage.sync.get("nmaps").then(lazyloadconfig)
|
||||
async function lazyloadconfig(config_obj){
|
||||
let nmaps_config = config_obj["nmaps"]
|
||||
nmaps_config = (nmaps_config == undefined) ? {} : nmaps_config
|
||||
nmaps = merge_objects(nmaps, nmaps_config)
|
||||
async function lazyloadconfig(storageResult){
|
||||
nmaps = Object.assign(Object.create(null), DEFAULTNMAPS, storageResult.nmaps)
|
||||
console.log(nmaps)
|
||||
}
|
||||
|
||||
|
@ -66,13 +67,6 @@ browser.storage.onChanged.addListener(
|
|||
}
|
||||
})
|
||||
|
||||
// Shamelessly copied from https://plainjs.com/javascript/utilities/merge-two-javascript-objects-19/
|
||||
// It should be possible to replace this with Object.assign, but that doesn't seem to work :/
|
||||
function merge_objects(obj,src){
|
||||
Object.keys(src).forEach(function(key) { obj[key] = src[key]; });
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Split a string into a number prefix and some following keys.
|
||||
function keys_split_count(keys: string[]){
|
||||
// Extracts the first number with capturing parentheses
|
||||
|
@ -108,7 +102,7 @@ function possible_maps(keys): string[] {
|
|||
let [count, keystr] = keys_split_count(keys)
|
||||
|
||||
// Short circuit or search maps.
|
||||
if (nmaps.hasOwnProperty(keystr)) {
|
||||
if (Object.getOwnPropertyNames(nmaps).includes(keystr)) {
|
||||
return [keystr,]
|
||||
} else {
|
||||
// Efficiency: this can be short-circuited.
|
||||
|
|
|
@ -13,15 +13,15 @@ In the meantime, here are some notes about Tridactyl:
|
|||
Highlighted features:
|
||||
|
||||
- Press `b` to bring up a list of open tabs in the current window; you can type the tab ID or part of the title or URL to choose a tab (the buffer list doesn't show which one you've selected yet, but it does work)
|
||||
- Press `f` to start "hint mode".
|
||||
- Press `I` to enter ignore mode. `Shift` + `Escape` to return to normal mode.
|
||||
- Press `f` to start "hint mode", `F` to open in background
|
||||
- Press `o` to `:open` a different page
|
||||
- Press `s` if you want to search for something that looks like a domain name or URL
|
||||
- Bind new commands with e.g. `:bind J tabprev`
|
||||
- If you want a bind to insert something on the commandline and wait, use `:bind <whatever> fillcmdline <whatever>` (observe the difference between `:bind t tabopen` and `:bind t fillcmdline tabopen`
|
||||
- Type `:help` to see a list of available excmds.
|
||||
- Use `yy` to copy the current page URL to your clipboard.
|
||||
- Bind new commands with e.g. `:bind J tabprev`. Type `:help bind` to see help on custom binds.
|
||||
- Type `:help` for online help
|
||||
- Use `yy` to copy the current page URL to your clipboard
|
||||
- `]]` and `[[` to navigate through the pages of comics, paginated articles, etc.
|
||||
- Pressing `ZZ` will close all tabs and windows, but it will only "save" them if your about:preferences are set to "show your tabs and windows from last time".
|
||||
- Pressing `ZZ` will close all tabs and windows, but it will only "save" them if your about:preferences are set to "show your tabs and windows from last time"
|
||||
|
||||
There are some caveats common to all webextension vimperator-alikes:
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue