Merge pull request #2 from tridactyl/master

Update to current Tridactyl repo
This commit is contained in:
notJerl 2019-10-20 23:09:23 -06:00 committed by GitHub
commit e7a945cf9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
184 changed files with 22903 additions and 20776 deletions

View file

@ -17,16 +17,16 @@ init:
- ps: Write-Host "[+] Location of Bash ..."
- ps: Get-Command -Name 'bash'
# Verify NPM
- ps: Write-Host "[+] Location of NPM ..."
- ps: Get-Command -Name 'npm'
# Verify yarn
- ps: Write-Host "[+] Location of yarn ..."
- ps: Get-Command -Name 'yarn'
# Verify software versions
- ps: Write-Host "[+] Verifying software verisons ..."
- sh --version
- bash --version
- node --version
- npm --version
- yarn --version
#
# Python version will show "2.7" below, which is required to keep
@ -72,8 +72,8 @@ install:
- ps: Get-Location
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && ls -alh"
# Install NPM modules
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && npm install"
# Install yarn modules
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && yarn install"
build_script:
# Add Python-3.6 to %PATH%
@ -101,8 +101,8 @@ build_script:
- ps: Write-Host "[+] Current directory under Bash ..."
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && ls -alh"
- ps: Write-Host "[+] Starting 'npm run build' ..."
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && export PYINSTALLER=1 && npm run build"
- ps: Write-Host "[+] Starting 'yarn run build' ..."
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && export PYINSTALLER=1 && yarn run build"
test_script:
# Add Python-3.6 to %PATH%
@ -130,5 +130,5 @@ test_script:
- ps: Write-Host "[+] Current directory under Bash ..."
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && ls -alh"
- ps: Write-Host "[+] Starting 'npm run test' ..."
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && export PYINSTALLER=1 && npm run test"
- ps: Write-Host "[+] Starting 'yarn run test' ..."
- bash -e -l -c "cd $APPVEYOR_BUILD_FOLDER && export PYINSTALLER=1 && yarn run test"

51
.circleci/config.yml Normal file
View file

@ -0,0 +1,51 @@
version: 2.1
commands:
commoncmd:
steps:
- checkout
- restore_cache:
keys:
- dependency-cache-{{ checksum "package.json" }}
- run: yarn install
- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
paths:
- ./node_modules
- run: yarn run build
jobs:
lint:
docker:
- image: circleci/node:latest
steps:
- run: sudo apt update
- run: sudo apt install -qq shellcheck
- commoncmd
- run: bash -c 'GLOBIGNORE="node_modules" shellcheck -e2012 **/*.sh'
- run: yarn run lint
- run: bash -c '"$(yarn bin)/tslint" --project .'
unit:
docker:
- image: circleci/node:latest
steps:
- commoncmd
- run: bash -c '"$(yarn bin)/jest" src'
e2e:
docker:
- image: circleci/node:latest-browsers
environment:
MOZ_HEADLESS: 1
steps:
- commoncmd
- run: sudo yarn global add get-firefox
- run: get-firefox --branch nightly --platform linux --extract --target ~/
- run: ~/firefox/firefox -v
- run: bash -c '"$(yarn bin)/web-ext" build --source-dir ./build --overwrite-dest'
- run: mv web-ext-artifacts/*.zip web-ext-artifacts/tridactyl.xpi
- run: bash -c 'PATH="$HOME/firefox:$PATH" "$(yarn bin)/jest"'
workflows:
version: 2
build_test_lint:
jobs:
- lint
- unit
- e2e

6
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,6 @@
# These are supported funding model platforms
# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: bovine3dom
patreon: tridactyl
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY

8
.gitignore vendored
View file

@ -1,17 +1,15 @@
AMOKEYS
build
node_modules
src/.excmds_background.generated.ts
src/.excmds_content.generated.ts
src/grammars/*.ts
generated
web-ext-artifacts
yarn.lock
.vscode
native/__pycache__
native/native_main
native_main.spec
.wine-pyinstaller
tags
compiler/gen_metadata.js
src/.metadata.generated.ts
compiler/*.js
compiler/**/*.js
.*.generated.ts

View file

@ -1,14 +0,0 @@
language: node_js
node_js:
- node
cache:
directories:
- node_modules
notifications:
webhooks:
urls:
- secure: JMDwcT11GIiLITD48xmVzJtncMPBr/EU0BXCn30oD/i/+tYnAlg/08/6zf22x08cy34saxDEy4+fQnpRG1ConITb2eAGvh2olTuQCIGUYMQvGzd5BX23keNe7VbflJrKKRdbhOZ0wPJRb7mUPbtFbKlAl35WnGMaf/3Dz1yfAx/znBVmAoPQ0LN+VLfp2KzIFAclWhtZluJjbKgFixZCbrS/6tQruYQnWN2+6LPR+jcxltpQaRsXy6HGg9kGGPZCBbPOVZl5PCpkrFCilzD4OQTy7Vc9i5xFBIM9lVDA5/K5l4eEtAMcvCHUz2HdGyJwK5eAPYVeXZYjIdrEEbjxstUuj1Y6aCJYuJAb9wKYCELRDP4XXl/8qR0QUF3VrvADe0tsaYzJow/CvzZ7/8zEzYmn0FO27BC/7mQBgMROucswbRD/BEAkZwXtk5CFFTffZX+daZ1Kfe8uAGlnCPZQWeUUmsiLe34VbKjEjzQ2aP7K+0vHgQQUtxoA2Cey6N90PCSMFklHDAy2Rv+7aPUXNu+zOFSmPhurL0/P5cu3uovHWitc+gEd1mGHSY5XIe+n2zz1bY6j9sx4O8mEhTBRRDTOSIbmv8kEhVcTvy3FQdyw5NYCTkdjy6MNLHXtjsQjTZmY72o3ZtaSxQCEAQ3c1SLUGpnpqx2qPb7t8mxuiSc=
on_success: change # always|never|change
on_failure: always
on_start: never

View file

@ -1,9 +1,25 @@
" bovine3dom's dogfood
" WARNING: This file defines and runs a command called fixamo_quiet. If you
" also have a malicious addon that operates on `<all_urls>` installed this
" will allow it to steal your firefox account credentials!
"
" With those credentials, an attacker can read anything in your sync account,
" publish addons to the AMO, etc, etc.
"
" Without this command a malicious addon can steal credentials from any site
" that you visit that is not in the restrictedDomains list.
"
" You should comment out the fixamo lines unless you are entirely sure that
" they are what you want.
"
" The advantage of running the command is that you can use the tridactyl
" interface on addons.mozilla.org and other restricted sites.
" Provided only as an example.
" Do not install/run without reading through as you may be surprised by some
" of the settings.
" May require the latest beta builds.
" Move this to $XDG_CONFIG_DIR/tridactyl/tridactylrc (that's
@ -11,7 +27,7 @@
" install the native messenger (:installnative in Tridactyl). Run :source to
" get it in the browser, or just restart.
" NB: If you want "vim-like" behaviour where removing a line from
" NB: If you want "vim-like" behaviour where removing a line from
" here makes the setting disappear, uncomment the line below.
"sanitise tridactyllocal tridactylsync
@ -26,6 +42,47 @@ bind ;c hint -c [class*="expand"],[class="togg"]
" GitHub pull request checkout command to clipboard (only works if you're a collaborator or above)
bind yp composite js document.getElementById("clone-help-step-1").textContent.replace("git checkout -b", "git checkout -B").replace("git pull ", "git fetch ") + "git reset --hard " + document.getElementById("clone-help-step-1").textContent.split(" ")[3].replace("-","/") | yank
" Git{Hub,Lab} git clone via SSH yank
bind yg composite js "git clone " + document.location.href.replace(/https?:\/\//,"git@").replace("/",":").replace(/$/,".git") | clipboard yank
" As above but execute it and open terminal in folder
bind ,g js let uri = document.location.href.replace(/https?:\/\//,"git@").replace("/",":").replace(/$/,".git"); tri.native.run("cd ~/projects; git clone " + uri + "; cd \"$(basename \"" + uri + "\" .git)\"; st")
" I like wikiwand but I don't like the way it changes URLs
bindurl wikiwand.com yy composite js document.location.href.replace("wikiwand.com/en","wikipedia.org/wiki") | clipboard yank
" Make gu take you back to subreddit from comments
bindurl reddit.com gu urlparent 4
" Only hint search results on Google and DDG
bindurl www.google.com f hint -Jc .rc > .r > a
bindurl www.google.com F hint -Jbc .rc>.r>a
bindurl ^https://duckduckgo.com f hint -Jc [class=result__a]
bindurl ^https://duckduckgo.com F hint -Jbc [class=result__a]
" Allow Ctrl-a to select all in the commandline
unbind --mode=ex <C-a>
" Allow Ctrl-c to copy in the commandline
unbind --mode=ex <C-c>
" Handy multiwindow/multitasking binds
bind gd tabdetach
bind gD composite tabduplicate | tabdetach
" Make yy use canonical / short links on the 5 websites that support them
bind yy clipboard yankshort
" Stupid workaround to let hint -; be used with composite which steals semi-colons
command hint_focus hint -;
" Open right click menu on links
bind ;C composite hint_focus; !s xdotool key Menu
" Julia docs' built in search is bad
set searchurls.julia https://www.google.com/search?q=site:http://docs.julialang.org/en/v1.0%20
"
" Misc settings
"
@ -38,23 +95,31 @@ jsb browser.runtime.getPlatformInfo().then(os=>{const profiledir = os.os=="win"
" Sane hinting mode
set hintfiltermode vimperator-reflow
set hintchars 4327895610
set hintnames numeric
" Make Tridactyl work on more sites at the expense of some security
set csp clobber
" Defaults to 300ms but I'm a 'move fast and close the wrong tabs' kinda chap
set hintdelay 100
" Add helper commands that Mozillians think make Firefox irredeemably
" insecure. For details, read the comment at the top of this file.
command fixamo_quiet jsb tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""'))
command fixamo js tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""').then(tri.excmds.fillcmdline_tmp(3000, "Permissions added to user.js. Please restart Firefox to make them take affect.")))
" Make Tridactyl work on more sites at the expense of some security. For
" details, read the comment at the top of this file.
fixamo_quiet
" Make quickmarks for the sane Tridactyl issue view
quickmark t https://github.com/cmcaine/tridactyl/issues?utf8=%E2%9C%93&q=sort%3Aupdated-desc+
quickmark t https://github.com/tridactyl/tridactyl/issues?utf8=%E2%9C%93&q=sort%3Aupdated-desc+
"
" URL redirects
"
"
" New reddit is bad
autocmd DocStart www.reddit.com js tri.excmds.urlmodify("-t", "www", "old")
autocmd DocStart ^http(s?)://www.reddit.com js tri.excmds.urlmodify("-t", "www", "old")
" Mosquito nets won't make themselves
autocmd DocStart www.amazon.co.uk js tri.excmds.urlmodify("-t", "www", "smile")
autocmd DocStart ^http(s?)://www.amazon.co.uk js tri.excmds.urlmodify("-t", "www", "smile")
" This will have to do until someone writes us a nice syntax file :)
" vim: set filetype=vim:

File diff suppressed because it is too large Load diff

View file

@ -1,95 +1,97 @@
import * as ts from "typescript"
import * as fs from "fs"
import * as commandLineArgs from "command-line-args"
import * as AllTypes from "./types/AllTypes"
import * as AllMetadata from "./metadata/AllMetadata"
class SimpleType {
name: string
kind: string
// Support for generics is mostly done but it might not be needed for now
// generics: Array<SimpleType>
arguments: Array<SimpleType>
type: SimpleType
constructor(typeNode) {
switch (typeNode.kind) {
case ts.SyntaxKind.VoidKeyword:
this.kind = "void"
break
case ts.SyntaxKind.AnyKeyword:
this.kind = "any"
break
case ts.SyntaxKind.BooleanKeyword:
this.kind = "boolean"
break
case ts.SyntaxKind.NumberKeyword:
this.kind = "number"
break
case ts.SyntaxKind.ObjectKeyword:
this.kind = "object"
break
case ts.SyntaxKind.StringKeyword:
this.kind = "string"
break
case ts.SyntaxKind.Parameter:
// 149 is "Parameter". We don't care about that so let's
// convert its type into a SimpleType and grab what we need from it
let ttype = new SimpleType(typeNode.type)
this.kind = ttype.kind
// this.generics = ttype.generics
this.arguments = ttype.arguments
this.type = ttype.type
this.name = typeNode.name.original.escapedText
break
case ts.SyntaxKind.TypeReference:
// 162 is "TypeReference". Not sure what the rules are here but it seems to be used for generics
this.kind = typeNode.typeName.escapedText
if (typeNode.typeArguments) {
this.arguments = typeNode.typeArguments.map(
t => new SimpleType(typeNode.typeArguments[0]),
export function toSimpleType(typeNode) {
switch (typeNode.kind) {
case ts.SyntaxKind.VoidKeyword:
return new AllTypes.VoidType()
// IndexedAccessTypes are things like `fn<T keyof Class>(x: T, y: Class[T])`
// This doesn't seem to be easy to deal with so let's kludge it for now
case ts.SyntaxKind.IndexedAccessType:
// Unknown is just like any, but slightly stricter
case ts.SyntaxKind.UnknownKeyword:
case ts.SyntaxKind.AnyKeyword:
return new AllTypes.AnyType()
case ts.SyntaxKind.BooleanKeyword:
return new AllTypes.BooleanType()
case ts.SyntaxKind.NumberKeyword:
return new AllTypes.NumberType()
case ts.SyntaxKind.ObjectKeyword:
return new AllTypes.ObjectType()
case ts.SyntaxKind.StringKeyword:
return new AllTypes.StringType()
case ts.SyntaxKind.Parameter:
let n = toSimpleType(typeNode.type)
n.name = typeNode.name.original.escapedText
n.isDotDotDot = !!typeNode.dotDotDotToken
n.isQuestion = !!typeNode.questionToken
return n
case ts.SyntaxKind.TypeReference:
if (!typeNode.typeArguments) {
// If there are no typeArguments, this is not a parametric type and we can return the type directly
try {
return toSimpleType(
typeNode.typeName.symbol.declarations[0].type,
)
} catch (e) {
// Fall back to what you'd do with typeArguments
}
break
case ts.SyntaxKind.FunctionType:
this.kind = "function"
// Probably don't need generics for now
// this.generics = (typeNode.typeParameters || []).map(p => new SimpleType(p))
this.arguments = typeNode.parameters.map(p => new SimpleType(p))
this.type = new SimpleType(typeNode.type)
break
case ts.SyntaxKind.TypeLiteral:
// This is a type literal. i.e., something like this: { [str: string]: string[] }
// Very complicated and perhaps not useful to know about. Let's just say "object" for now
this.kind = "object"
break
case ts.SyntaxKind.ArrayType:
this.kind = "array"
this.type = new SimpleType(typeNode.elementType)
break
case ts.SyntaxKind.TupleType:
this.kind = "tuple"
this.arguments = typeNode.elementTypes.map(
t => new SimpleType(t),
)
break
case ts.SyntaxKind.UnionType:
this.kind = "union"
this.arguments = typeNode.types.map(t => new SimpleType(t))
break
case ts.SyntaxKind.LiteralType:
// "LiteralType". I'm not sure what this is. Probably things like type a = "b" | "c"
this.kind = "LiteralType"
this.name = typeNode.literal.text
break
default:
console.log(typeNode)
throw new Error(
`Unhandled kind (${typeNode.kind}) for ${typeNode}`,
)
}
}
let args = typeNode.typeArguments
? typeNode.typeArguments.map(t =>
toSimpleType(typeNode.typeArguments[0]),
)
: []
return new AllTypes.TypeReferenceType(
typeNode.typeName.escapedText,
args,
)
case ts.SyntaxKind.FunctionType:
// generics = (typeNode.typeParameters || []).map(p => new AllTypes.SimpleType(p))
return new AllTypes.FunctionType(
typeNode.parameters.map(p => toSimpleType(p)),
toSimpleType(typeNode.type),
)
case ts.SyntaxKind.TypeLiteral:
let members = typeNode.members
.map(member => {
if (member.kind == ts.SyntaxKind.IndexSignature) {
// Something like this: { [str: string]: string[] }
return ["", toSimpleType(member.type)]
}
// Very fun feature: when you have an object literal with >20 members, typescript will decide to replace some of them with a "... X more ..." node that obviously doesn't have a corresponding symbol, hence this check and the filter after the map
if (member.name.symbol)
return [
member.name.symbol.escapedName,
toSimpleType(member.type),
]
})
.filter(m => m)
return new AllTypes.ObjectType(new Map(members))
case ts.SyntaxKind.ArrayType:
return new AllTypes.ArrayType(toSimpleType(typeNode.elementType))
case ts.SyntaxKind.TupleType:
return new AllTypes.TupleType(
typeNode.elementTypes.map(t => toSimpleType(t)),
)
case ts.SyntaxKind.UnionType:
return new AllTypes.UnionType(
typeNode.types.map(t => toSimpleType(t)),
)
break
case ts.SyntaxKind.LiteralType:
return new AllTypes.LiteralTypeType(typeNode.literal.text)
break
default:
console.log(typeNode)
throw new Error(`Unhandled kind (${typeNode.kind}) for ${typeNode}`)
}
}
/** True if this is visible outside this file, false otherwise */
/** True if node is visible outside its file, false otherwise */
function isNodeExported(node: ts.Node): boolean {
return (
(ts.getCombinedModifierFlags(<ts.Declaration>node) &
@ -99,57 +101,89 @@ function isNodeExported(node: ts.Node): boolean {
)
}
function visit(checker: any, filename: string, node: any, everything: any) {
/** True if node is marked as @hidden in its documentation */
function isNodeHidden(sourceFile, node): boolean {
return (
sourceFile &&
node.jsDoc &&
!!node.jsDoc.find(
doc =>
sourceFile.text.slice(doc.pos, doc.end).search("@hidden") != -1,
)
)
}
function visit(
checker: any,
sourceFile: any,
file: AllMetadata.FileMetadata,
node: any,
) {
let symbol = checker.getSymbolAtLocation(node.name)
if (symbol && isNodeExported(node)) {
// ensure() is very simple, it just creates a key named `name` the value of which is `def` if `name` doesn't exist in `obj`
let ensure = (obj, name, def) => {
obj[name] = obj[name] || def
return obj[name]
}
// addDoc creates a "doc" key set to an empty array in `obj` if it doesn't exist and then adds documentation from the symbol to it if it isn't already in the array
let addDoc = (obj, symbol) => {
let doc = ensure(obj, "doc", [])
let docstr = ts.displayPartsToString(
symbol.getDocumentationComment(),
)
if (docstr && !doc.includes(docstr)) doc.push(docstr)
}
// addType sets the `type` attribute of `obj` to the SimpleType of `symbol` if it has one
let addType = (obj, symbol) => {
let ttype = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!,
)
if (ttype) {
obj["type"] = new SimpleType(checker.typeToTypeNode(ttype))
}
}
let nodeName = symbol.escapedName
let file = ensure(everything, filename, {})
switch (node.kind) {
case ts.SyntaxKind.FunctionDeclaration:
let functions = ensure(file, "functions", {})
let func = ensure(functions, nodeName, {})
addDoc(func, symbol)
addType(func, symbol)
break
// Grab the doc, default to empty string
let doc =
ts.displayPartsToString(symbol.getDocumentationComment()) ||
""
// Grab the type
let ttype = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!,
)
// If the function has a type, try to convert it, if it doesn't, default to any
let t = ttype
? toSimpleType(checker.typeToTypeNode(ttype))
: new AllTypes.AnyType()
file.setFunction(
nodeName,
new AllMetadata.SymbolMetadata(
doc,
t,
isNodeHidden(sourceFile, node),
),
)
return
case ts.SyntaxKind.ClassDeclaration:
let classes = ensure(file, "classes", {})
let clazz = ensure(classes, nodeName, {})
let clazz = file.getClass(nodeName)
if (!clazz) {
clazz = new AllMetadata.ClassMetadata()
file.setClass(nodeName, clazz)
}
symbol.members.forEach((sym, name, map) => {
// Can't get doc/type from these special functions
// Or at least, it requires work that might not be needed for now
if (["__constructor", "get", "set"].includes(name)) return
let member = ensure(clazz, name, {})
addDoc(member, sym)
addType(member, sym)
// Grab the doc, default to empty string
let doc =
ts.displayPartsToString(
sym.getDocumentationComment(),
) || ""
// Grab the type
let ttype = checker.getTypeOfSymbolAtLocation(
sym,
sym.valueDeclaration!,
)
// If the function has a type, try to convert it, if it doesn't, default to any
let t = ttype
? toSimpleType(checker.typeToTypeNode(ttype))
: new AllTypes.AnyType()
clazz.setMember(
name,
new AllMetadata.SymbolMetadata(
doc,
t,
isNodeHidden(sourceFile, node),
),
)
})
break
return
// Other declaration syntaxkinds:
// case ts.SyntaxKind.VariableDeclaration:
// case ts.SyntaxKind.VariableDeclarationList:
@ -171,7 +205,7 @@ function visit(checker: any, filename: string, node: any, everything: any) {
}
}
ts.forEachChild(node, node => visit(checker, filename, node, everything))
ts.forEachChild(node, node => visit(checker, sourceFile, file, node))
}
function generateMetadata(
@ -185,18 +219,34 @@ function generateMetadata(
module: ts.ModuleKind.CommonJS,
})
let everything = {}
let metadata = new AllMetadata.ProgramMetadata()
for (const sourceFile of program.getSourceFiles()) {
let n = (fileNames as any).find(name => sourceFile.fileName.match(name))
if (n) visit(program.getTypeChecker(), n, sourceFile, everything)
let name = (fileNames as any).find(name =>
sourceFile.fileName.match(name),
)
if (name) {
let file = metadata.getFile(name)
if (!file) {
file = new AllMetadata.FileMetadata()
metadata.setFile(name, file)
}
visit(program.getTypeChecker(), sourceFile, file, sourceFile)
}
}
let metadataString = `\nexport let everything = ${JSON.stringify(
everything,
undefined,
4,
)}\n`
// We need to specify Type itself because it won't exist in AllTypes.js since it's an interface
let imports =
`import { Type } from "../compiler/types/AllTypes"\n` +
`import {${Object.keys(AllTypes).join(
", ",
)}} from "../compiler/types/AllTypes"\n` +
`import {${Object.keys(AllMetadata).join(
", ",
)}} from "../compiler/metadata/AllMetadata"\n`
let metadataString =
imports + `\nexport let everything = ${metadata.toConstructor()}\n`
if (themedir) {
metadataString += `\nexport let staticThemes = ${JSON.stringify(

View file

@ -0,0 +1,4 @@
export { SymbolMetadata } from "./SymbolMetadata"
export { ClassMetadata } from "./ClassMetadata"
export { FileMetadata } from "./FileMetadata"
export { ProgramMetadata } from "./ProgramMetadata"

View file

@ -0,0 +1,33 @@
import { SymbolMetadata } from "./SymbolMetadata"
export class ClassMetadata {
constructor(
public members: Map<string, SymbolMetadata> = new Map<
string,
SymbolMetadata
>(),
) {}
public setMember(name: string, s: SymbolMetadata) {
this.members.set(name, s)
}
public getMember(name: string) {
return this.members.get(name)
}
public getMembers() {
return this.members.keys()
}
public toConstructor() {
return (
`new ClassMetadata(new Map<string, SymbolMetadata>([` +
Array.from(this.members.entries())
.map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -0,0 +1,57 @@
import { ClassMetadata } from "./ClassMetadata"
import { SymbolMetadata } from "./SymbolMetadata"
export class FileMetadata {
constructor(
public classes: Map<string, ClassMetadata> = new Map<
string,
ClassMetadata
>(),
public functions: Map<string, SymbolMetadata> = new Map<
string,
SymbolMetadata
>(),
) {}
public setClass(name: string, c: ClassMetadata) {
this.classes.set(name, c)
}
public getClass(name: string) {
return this.classes.get(name)
}
public getClasses() {
return Array.from(this.classes.keys())
}
public setFunction(name: string, f: SymbolMetadata) {
this.functions.set(name, f)
}
public getFunction(name: string) {
return this.functions.get(name)
}
public getFunctions() {
return Array.from(this.functions.entries())
}
public getFunctionNames() {
return Array.from(this.functions.keys())
}
public toConstructor() {
return (
`new FileMetadata(new Map<string, ClassMetadata>([` +
Array.from(this.classes.entries())
.map(([n, c]) => `[${JSON.stringify(n)}, ${c.toConstructor()}]`)
.join(",\n") +
`]), new Map<string, SymbolMetadata>([` +
Array.from(this.functions.entries())
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -0,0 +1,28 @@
import { FileMetadata } from "./FileMetadata"
export class ProgramMetadata {
constructor(
public files: Map<string, FileMetadata> = new Map<
string,
FileMetadata
>(),
) {}
public setFile(name: string, file: FileMetadata) {
this.files.set(name, file)
}
public getFile(name: string) {
return this.files.get(name)
}
public toConstructor() {
return (
`new ProgramMetadata(new Map<string, FileMetadata>([` +
Array.from(this.files.entries())
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -0,0 +1,11 @@
import { Type } from "../types/AllTypes"
export class SymbolMetadata {
constructor(public doc: string, public type: Type, public hidden = false) {}
public toConstructor() {
return `new SymbolMetadata(${JSON.stringify(
this.doc,
)}, ${this.type.toConstructor()}, ${this.hidden})`
}
}

View file

@ -0,0 +1,13 @@
export { Type } from "./Type"
export { AnyType } from "./AnyType"
export { BooleanType } from "./BooleanType"
export { FunctionType } from "./FunctionType"
export { NumberType } from "./NumberType"
export { ObjectType } from "./ObjectType"
export { StringType } from "./StringType"
export { TypeReferenceType } from "./TypeReferenceType"
export { VoidType } from "./VoidType"
export { ArrayType } from "./ArrayType"
export { LiteralTypeType } from "./LiteralTypeType"
export { TupleType } from "./TupleType"
export { UnionType } from "./UnionType"

19
compiler/types/AnyType.ts Normal file
View file

@ -0,0 +1,19 @@
import { Type } from "./Type"
export class AnyType implements Type {
public kind = "any"
constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new AnyType(${!this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
return argument
}
}

View file

@ -0,0 +1,29 @@
import { Type } from "./Type"
export class ArrayType implements Type {
public kind = "array"
constructor(public elemType: Type, public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new ArrayType(${this.elemType.toConstructor()}, ${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return `${this.elemType.toString()}[]`
}
public convert(argument) {
if (!Array.isArray(argument)) {
try {
argument = JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert ${argument} to array:`)
}
if (!Array.isArray(argument)) {
throw new Error(`Can't convert ${argument} to array:`)
}
}
return argument.map(v => this.elemType.convert(v))
}
}

View file

@ -0,0 +1,24 @@
import { Type } from "./Type"
export class BooleanType implements Type {
public kind = "boolean"
constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new BooleanType(${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
if (argument === "true") {
return true
} else if (argument === "false") {
return false
}
throw new Error("Can't convert ${argument} to boolean")
}
}

View file

@ -0,0 +1,28 @@
import { Type } from "./Type"
export class FunctionType implements Type {
public kind = "function"
constructor(public args: Type[], public ret: Type, public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return (
`new FunctionType([` +
// Convert every argument type to its string constructor representation
this.args.map(cur => cur.toConstructor()) +
`], ${this.ret.toConstructor()}, ${this.isDotDotDot}, ${this.isQuestion})`
)
}
public toString() {
return `(${this.args.map(a => a.toString()).join(", ")}) => ${this.ret.toString()}`
}
public convert(argument) {
// Possible strategies:
// - eval()
// - window[argument]
// - tri.excmds[argument]
throw new Error(`Conversion to function not implemented: ${argument}`)
}
}

View file

@ -0,0 +1,26 @@
import { Type } from "./Type"
export class LiteralTypeType implements Type {
public kind = "LiteralType"
constructor(public value: string, public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new LiteralTypeType(${JSON.stringify(this.value)}, ${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return JSON.stringify(this.value)
}
public convert(argument) {
if (argument === this.value) {
return argument
}
throw new Error(
`Argument does not match expected value (${
this.value
}): ${argument}`,
)
}
}

View file

@ -0,0 +1,23 @@
import { Type } from "./Type"
export class NumberType implements Type {
public kind = "number"
public constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new NumberType(${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
const n = parseFloat(argument)
if (!Number.isNaN(n)) {
return n
}
throw new Error(`Can't convert to number: ${argument}`)
}
}

View file

@ -0,0 +1,43 @@
import { Type } from "./Type"
export class ObjectType implements Type {
public kind = "object"
// Note: a map that has an empty key ("") uses the corresponding type as default type
constructor(public members: Map<string, Type> = new Map<string, Type>(), public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new ObjectType(new Map<string, Type>([` +
Array.from(this.members.entries()).map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
.join(", ") +
`]), ${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convertMember(memberName: string[], memberValue: string) {
let type = this.members.get(memberName[0])
if (!type) {
// No type, try to get the default type
type = this.members.get("")
if (!type) {
// No info for this member and no default type, anything goes
return memberValue
}
}
if (type.kind === "object") {
return (type as ObjectType).convertMember(memberName.slice(1), memberValue)
}
return type.convert(memberValue)
}
public convert(argument) {
try {
return JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert to object: ${argument}`)
}
}
}

View file

@ -0,0 +1,22 @@
import { Type } from "./Type"
export class StringType implements Type {
public kind = "string"
constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new StringType(${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
if (typeof argument === "string") {
return argument
}
throw new Error(`Can't convert to string: ${argument}`)
}
}

View file

@ -0,0 +1,39 @@
import { Type } from "./Type"
export class TupleType implements Type {
public kind = "tuple"
constructor(public elemTypes: Type[], public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return (
`new TupleType([` +
// Convert every element type to its constructor representation
this.elemTypes.map(cur => cur.toConstructor()).join(",\n") +
`], ${this.isDotDotDot}, ${this.isQuestion})`
)
}
public toString() {
return `[${this.elemTypes.map(e => e.toString()).join(", ")}]`
}
public convert(argument) {
if (!Array.isArray(argument)) {
try {
argument = JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert to tuple: ${argument}`)
}
if (!Array.isArray(argument)) {
throw new Error(`Can't convert to tuple: ${argument}`)
}
}
if (argument.length !== this.elemTypes.length) {
throw new Error(
`Error converting tuple: number of elements and type mismatch ${argument}`,
)
}
return argument.map((v, i) => this.elemTypes[i].convert(v))
}
}

12
compiler/types/Type.ts Normal file
View file

@ -0,0 +1,12 @@
export interface Type {
// Only available on argument types
name?: string
isDotDotDot?: boolean
isQuestion?: boolean
// available everywhere
kind: string
toConstructor(): string
toString(): string
convert(argument: string): any
}

View file

@ -0,0 +1,22 @@
import { Type } from "./Type"
export class TypeReferenceType implements Type {
public constructor(public kind: string, public args: Type[], public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return (
`new TypeReferenceType(${JSON.stringify(this.kind)}, [` +
// Turn every type argument into its constructor representation
this.args.map(cur => cur.toConstructor()).join(",\n") +
`], ${this.isDotDotDot}, ${this.isQuestion})`
)
}
public toString() {
return `${this.kind}<${this.args.map(a => a.toString()).join(", ")}>`
}
public convert(argument) {
throw new Error("Conversion of simple type references not implemented.")
}
}

View file

@ -0,0 +1,29 @@
import { Type } from "./Type"
export class UnionType implements Type {
public kind = "union"
constructor(public types: Type[], public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return (
`new UnionType([` +
// Convert every type to its string constructor representation
this.types.map(cur => cur.toConstructor()).join(",\n") +
`], ${this.isDotDotDot}, ${this.isQuestion})`
)
}
public toString() {
return this.types.map(t => t.toString()).join(" | ")
}
public convert(argument) {
for (const t of this.types) {
try {
return t.convert(argument)
} catch (e) {}
}
throw new Error(`Can't convert "${argument}" to any of: ${this.types}`)
}
}

View file

@ -0,0 +1,19 @@
import { Type } from "./Type"
export class VoidType implements Type {
public kind = "void"
constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new VoidType(${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
return null
}
}

View file

@ -6,23 +6,23 @@ Tridactyl is very lucky to have a wide base of contributors, 30 at the time of w
### Quick tasks (~10 minutes)
* Leave a review on [addons.mozilla.org][amoreviews] (very few people do this :( )
* Tell your friends about us :)
* Read through [readme.md][readme], our [newtab.md][newtab] or our page on [addons.mozilla.org][amo] and see if anything looks out of date. If it does, file an issue or fork the repository (button in top right), fix it yourself (you can edit it using the pencil icon), and make a pull request.
- Leave a review on [addons.mozilla.org][amoreviews] (very few people do this :( )
- Tell your friends about us :)
- Read through [readme.md][readme], our [newtab.md][newtab] or our page on [addons.mozilla.org][amo] and see if anything looks out of date. If it does, file an issue or fork the repository (button in top right), fix it yourself (you can edit it using the pencil icon), and make a pull request.
### Quick tasks (~30 minutes)
* Run through `:tutor` and [tell us what you think][tutor] or make changes directly.
- Run through `:tutor` and [tell us what you think][tutor] or make changes directly.
## Programming (1 hour+)
* Take a look through the [open issues][issues] and then check with [pull requests][prs] to make sure that someone isn't already working on it. Please post in an issue to say that you're working on it.
* If you don't have much experience with JavaScript or WebExtensions, we purposefully leave some particularly simple issues open so that people can get started, and give them the tag [good first issue][easyissues]. Feel free to ask us any questions about the build process on [Matrix][matrix].
* If you have experience with JavaScript or WebExtensions, please look through the issues tagged [help wanted][helpus] as we're really stuck on them.
* You could work on some feature that you really want to see in Tridactyl that we haven't even thought of yet.
* Our build process is a bit convoluted, but [excmds.ts][excmds] is probably where you want to start. Most of the business happens there.
* We use TypeDoc to produce the `:help` page. Look at the other functions in [excmds.ts][excmds] to get an idea of how to use it; if your function is not supposed to called from the command line, then please add `/** @hidden */` above it to prevent it being shown on the help page.
* Our pre-commit hook runs prettier to format your code. Please don't circumvent it.
- Take a look through the [open issues][issues] and then check with [pull requests][prs] to make sure that someone isn't already working on it. Please post in an issue to say that you're working on it.
- If you don't have much experience with JavaScript or WebExtensions, we purposefully leave some particularly simple issues open so that people can get started, and give them the tag [good first issue][easyissues]. Feel free to ask us any questions about the build process on [Matrix][matrix].
- If you have experience with JavaScript or WebExtensions, please look through the issues tagged [help wanted][helpus] as we're really stuck on them.
- You could work on some feature that you really want to see in Tridactyl that we haven't even thought of yet.
- Our build process is a bit convoluted, but [excmds.ts][excmds] is probably where you want to start. Most of the business happens there.
- We use TypeDoc to produce the `:help` page. Look at the other functions in [excmds.ts][excmds] to get an idea of how to use it; if your function is not supposed to called from the command line, then please add `/** @hidden */` above it to prevent it being shown on the help page.
- Our pre-commit hook runs prettier to format your code. Please don't circumvent it.
If you are making a substantial or potentially controversial change, your first port of call should be to stop by and chat to us on [Matrix][matrix] or file an issue to discuss what you would like to change. We really don't want you to waste time on a pull request (GitHub jargon for a contribution) that has no chance of being merged; that said, we are probably happy to gate even the most controversial changes behind an option.
@ -30,22 +30,124 @@ If you are making a substantial or potentially controversial change, your first
Take a look in src/static/themes to get an idea of what to do. There is a reasonable amount of magic going on:
* All of your styles must be prefixed with `:root.TridactylTheme[Name]`. If your theme is called `bobstheme`, the selector mentioned must be `:root.TridactylThemeBobstheme` (note the capitalisation).
* All of your CSS will be injected into all pages, so it is important that is fenced off in this manner.
* `default.css` has loads of variables that you can use to make it easier for you to style things, and for your theme to apply to new elements that did not exist when you wrote your theme. It is advised that you make as much use of these as possible.
- All of your styles must be prefixed with `:root.TridactylTheme[Name]`. If your theme is called `bobstheme`, the selector mentioned must be `:root.TridactylThemeBobstheme` (note the capitalisation).
- All of your CSS will be injected into all pages, so it is important that is fenced off in this manner.
- `default.css` has loads of variables that you can use to make it easier for you to style things, and for your theme to apply to new elements that did not exist when you wrote your theme. It is advised that you make as much use of these as possible.
# Architecture of the project
WebExtensions have multiple kinds of [processes](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension) (or scripts). There's a background process, which is attached to the main Firefox process. There's also the content process, with at least one per tab (sometimes more, as content processes can live in frames too). There are other kinds of processes, but Tridactyl doesn't use them.
As of January 2019, Tridactyl uses 2n+1 processes: a background process and two content processes per tab (one for the page and one for the command line frame). These processes do not have the same privileges or access to APIs, they instead need to cooperate by sending messages to each other. Mozilla's API for sending messages is [browser.runtime.sendMessage](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage). You will probably not need to use it directly: Tridactyl has its own [message](https://github.com/tridactyl/tridactyl/blob/bdaa65d216678776b0406f5be99800ef5dd8d50f/src/lib/messaging.ts#L67), [messageActiveTab](https://github.com/tridactyl/tridactyl/blob/bdaa65d216678776b0406f5be99800ef5dd8d50f/src/lib/messaging.ts#L73) and [messageOwnTab](https://github.com/tridactyl/tridactyl/blob/master/src/lib/messaging.ts) functions, which are themselves used by higher-level abstractions such as [browserBg](https://github.com/tridactyl/tridactyl/blob/bdaa65d216678776b0406f5be99800ef5dd8d50f/src/lib/browser_proxy.ts#L3), the [macro preprocessor](https://github.com/tridactyl/tridactyl/blob/master/scripts/excmds_macros.py) and the [ex-mode dispatcher](https://github.com/tridactyl/tridactyl/blob/bdaa65d216678776b0406f5be99800ef5dd8d50f/src/excmds.ts#L2603).
## browserBg
The browserBg object is a simple proxy that enables calling any API available in the background process directly from the content script. For example, if you want to call the `browser.runtime.getPlatformInfo` function from the content script, just use `browserBg.runtime.getPlatformInfo()`. The one difference between `browser` and `browserBg` is that while `browser` has a few functions that do not return promises, `browserBg` will always return promises.
## The macros
The macro preprocessor's goal is to make content-script functions defined in [src/excmds.ts](https://github.com/tridactyl/tridactyl/blob/master/src/excmds.ts) available to the background script. It does so by reading `src/excmds.ts` and generating two files: `src/.excmds_background.generated.ts` and `src/.excmds_content.generated.ts`. While `src/.excmds_content.generated.ts` will only contain functions from `src/excmds.ts` marked with either `//#content` or `//#content_helper`, `src/.excmds_background.ts` will contain functions marked with `//#background` or `//#background_helper` and shims to automatically call `//#content` functions in the currently active tab.
Here's an example: you're writing the [`native()`](https://github.com/tridactyl/tridactyl/blob/bdaa65d216678776b0406f5be99800ef5dd8d50f/src/excmds.ts#L470) function in `excmds.ts` that checks whether the native messenger is installed. You need to use the native messaging API, which is only available in the background script, so you prepend your function declaration with the `//#background` macro. In order to let the user know whether the native messenger is installed or not, you will need to send a message to the content script of the currently activated tab in order to ask it to run the [`fillcmdline()`](https://github.com/tridactyl/tridactyl/blob/bdaa65d216678776b0406f5be99800ef5dd8d50f/src/excmds.ts#L2540) function. Since `fillcmdline` is marked with `//#content`, you can do this seamlessly just by calling `fillcmdline()` from the background script.
## Role of each file
### src/background/
- config_rc.ts: Functions related to loading and executing the tridactylrc.
- controller_background.ts: Parses and executes ex commands.
- download_background.ts: Utility functions related to downloading that have to live in the background because downloading APIs aren't available to other processes.
- hinting.ts: A simple proxy which just forwards ex command calls to the content script.
### src/content/
- commandline_content.ts: Functions to interact with the command line frame from the page (e.g. setting the iframe's height).
- controller_content.ts: Contains the logic for dispatching ex-commands on key presses and preventing pages from reading key events.
- finding.ts: Code related to the `:find` and `:findnext` commands.
- hinting.ts: Meat of the `:hint` ex command.
- scrolling.ts: Scrolling logic.
- state.ts: Functions to work with Tridactyl's per-tab state (e.g. mode).
- styling.ts: Functions to apply styles to Tridactyl's elements and to the page.
### src/lib/
- aliases.ts: Functions to resolve alias<->excmd mappings.
- autocontainers.ts: Classes and interfaces for autocontainers (who would have thought?).
- browser_proxy.ts: The implementation of the browserBg object.
- config.ts: Defines Tridactyl's settings and functions to retrieve them.
- containers.ts: Type definitions and wrappers around Firefox's container API.
- convert.ts: Conversion functions used in controller_background.ts for ex command dispatch.
- css_util.ts: CSS functions mostly used by :guiset.
- dom.ts: Various utility functions that operate on the dom.
- editor.ts: Implementation of readline functions available under the "text." namespace.
- html-tagged-template.ts: Tagged template mostly used in completion sources.
- itertools.ts: Function to work with JavaScript iterators (zip, map...).
- keyseq.ts: Functions and classes to parse, create and interact with key sequences (e.g. `<C-e>a`).
- logging.ts: Tridactyl's logging interfaces.
- math.ts: Math stuff.
- messaging.ts: Implementation of Tridactyl's messaging functions (attributeCaller, message, messageTab, messageOwnTab...).
- native.ts: Wrappers around Firefox's native messaging API. Also has "higher-level" functions that interact with the native messenger (finding the user's favorite editor, reading/setting preferences...).
- requests.ts: CSP-clobbering code. Not used anymore.
- text_to_speech.ts: Various wrappers around Firefox's TTS APIs.
- url_util.ts: Url incrementation, query-extraction, interpolation.
- webext.ts: Wrappers around Firefox's APIs (activeTab(), ownTab()...).
- nearley_utils.ts: Remnant of Tridactyl's previous architecture, where keys were handled in the background script.
### src/
- background.ts: Entry point of Tridactyl's background script. Deals with various things that didn't deserve their own file when they were implemented: autocommands, autocontainers...
- commandline_frame.ts: Entry point of the command line. Sets up various event listeners and updates completions when needed.
- completions/\*.ts: All completion sources available to Tridactyl. Imported by commandline_frame.ts
- completions.ts: Scaffolding used by completion sources in the "completions" folder.
- content.ts: Entry point of the content script. Does various things that should happen when a new tab is created (hijacking event listeners, adding the modeindicator to the page...).
- excmds.ts: All excmds, no matter whether they live in the content or background script. See the "The macros" section in order to learn a bit more about how they work.
- grammars/bracketexpr.ne: Defines the key sequence (e.g. `<C-a>`) parser
- help.ts: Script that is only included in help pages. Does things like embedding keybindings/settings values in the page.
- manifest.json: The webextension manifest file that defines specifies Tridactyl's content and background scripts, permissions, icons and a few other things.
- newtab.ts: Script that is only included in the newtab page. Currently only highlights the changelog when it changed since you last read it.
- parsers/\*: Defines the parsers that turn key bindings into ex commands.
- perf.ts: Performance-measuring tools.
- state.ts: Defines Tridactyl's global state (list of inputs, command history...).
- tridactyl.d.ts: Type definitions.
### src/static/
- authors.html: Template for the `:authors` page.
- badges/\*: Svg files embeded in the readme.
- clippy/\*: Tutorial files accessed with `:tutor`.
- commandline.html: Content of the comand line iframe.
- css: Global css files that apply to elements no matter what the current theme is.
- defaultFavicon.svg: The favicon tridactyl uses when it can't access the favicon of a tab.
- logo/\*: Tridactyl's logo in various resolutions.
- newtab.md: The content of Tridactyl's newtab page.
- newtab.template.html: Tridactyl's newtab page, without its content.
- themes/\*: Css files for each theme.
- typedoc: Typedoc templates and css.
# Build Process
Building Tridactyl is done with `yarn run build`. This makes yarn run [scripts/build.sh](https://github.com/tridactyl/tridactyl/blob/master/scripts/build.sh), which performs the following steps:
- Running the [macro preprocessor](https://github.com/tridactyl/tridactyl/blob/master/scripts/excmds_macros.py) to turn `src/excmds.ts` into `src/.excmds_background.ts` and `src/.excmds_content.ts` (see the "The macros" section for more info).
- Running the [metadata-generation](https://github.com/tridactyl/tridactyl/blob/master/compiler/gen_metadata.ts) which just re-injects type information and comment strings into Tridactyl's code in order to make them available to Tridactyl at runtime. It also checks what themes are available at compile time and adds this information to the metadata.
- Running webpack in order to compile Tridactyl down to one file per entry point.
- Generating the newtab, author and tutorial pages with custom scripts and the documentation using typedoc.
- Importing CSS files and embedding resources (other CSS files, base64 pictures) into them wherever they're needed
You can run Tridactyl easily in a temporary Firefox profile with `yarn run run`.
# Code of conduct
[Queensberry rules](https://en.oxforddictionaries.com/definition/queensberry_rules).
[matrix]: https://riot.im/app/#/room/#tridactyl:matrix.org
[issues]: https://github.com/cmcaine/tridactyl/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+
[easyissues]: https://github.com/cmcaine/tridactyl/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22
[helpus]: https://github.com/cmcaine/tridactyl/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
[prs]: https://github.com/cmcaine/tridactyl/pulls
[readme]: https://github.com/cmcaine/tridactyl/blob/master/readme.md
[issues]: https://github.com/tridactyl/tridactyl/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+
[easyissues]: https://github.com/tridactyl/tridactyl/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22
[helpus]: https://github.com/tridactyl/tridactyl/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
[prs]: https://github.com/tridactyl/tridactyl/pulls
[readme]: https://github.com/tridactyl/tridactyl/blob/master/readme.md
[amo]: https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/
[amoreviews]: https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/reviews/
[newtab]: https://github.com/cmcaine/tridactyl/blob/master/src/static/newtab.md
[tutor]: https://github.com/cmcaine/tridactyl/issues/380
[excmds]: https://github.com/cmcaine/tridactyl/blob/master/src/excmds.ts
[newtab]: https://github.com/tridactyl/tridactyl/blob/master/src/static/newtab.md
[tutor]: https://github.com/tridactyl/tridactyl/issues/380
[excmds]: https://github.com/tridactyl/tridactyl/blob/master/src/excmds.ts

View file

@ -2,27 +2,27 @@
Replace Firefox's control mechanism with one modelled on VIM. This is a "Firefox Quantum" replacement for VimFX, Vimperator and Pentadactyl. Most common tasks you want your browser to perform are bound to a single key press:
* You want to open a new tab? Hit `t`.
* You want to follow that link? Hit `f` and type the displayed label. (Note: hint characters should be typed in lowercase.)
* You want to go to the bottom of the page? Hit `G`. Or the top? `gg`.
* You want to focus the text field on Wikipedia to search for another term? `gi`.
* Switch to the next tab? `gt`.
* Go back in time? `H`.
* Notice that this tab is rubbish and you want to close it? `d`.
* Regret that decision? `u` restores it.
* Want to write something in Vim? `Ctrl-i` in a text box opens it in Vim, if you have `:native` working.
* Temporarily disable all that magic because you can't stand it? `Shift-Insert`.
* But how do you use your browser now? `Shift-Insert` again and we're back on.
- You want to open a new tab? Hit `t`.
- You want to follow that link? Hit `f` and type the displayed label. (Note: hint characters should be typed in lowercase.)
- You want to go to the bottom of the page? Hit `G`. Or the top? `gg`.
- You want to focus the text field on Wikipedia to search for another term? `gi`.
- Switch to the next tab? `gt`.
- Go back in time? `H`.
- Notice that this tab is rubbish and you want to close it? `d`.
- Regret that decision? `u` restores it.
- Want to write something in Vim? `Ctrl-i` in a text box opens it in Vim, if you have `:native` working.
- Temporarily disable all that magic because you can't stand it? `Shift-Insert`.
- But how do you use your browser now? `Shift-Insert` again and we're back on.
The list could go on a bit here, but I guess you'll get the point. If you feel lost sometimes `:help` might help you a lot, and there's always `:tutor`.
**Highlighted features:**
* follow any link on the site with just 2-3 key presses.
* switch to any open tab by searching for its URL or title or entering its ID.
* easy customizable search settings
* bind any supported command or commands to the key (sequence) of your liking
* great default bindings (if you're used to Pentadactyl or Vimperator)
- follow any link on the site with just 2-3 key presses.
- switch to any open tab by searching for its URL or title or entering its ID.
- easy customizable search settings
- bind any supported command or commands to the key (sequence) of your liking
- great default bindings (if you're used to Pentadactyl or Vimperator)
This add-on is very usable, but is in an early stage of development. We intend to implement the majority of Vimperator's features.
@ -32,27 +32,31 @@ You can get beta builds from [our website][betas].
Since Tridactyl aims to provide all the features Vimperator and Pentadactyl had, it requires quite a few permissions. Here we describe the specific permissions and why we need them.
* Access your data for all websites:
* This is Mozilla's way of saying that Tridactyl can read the content of web pages. This is necessary in order to e.g. find the links you can follow with the `:hint` command (bound to `f` by default).
* Exchange messages with programs other than Firefox
* This permission is required for Tridactyl to interact with your operating system (opening your editor to edit text areas, sending links to your video player, reading a configuration file from your disk...). This is possible thanks to an external executable we provide. If you feel this gives Tridactyl too much power you can chose not to install the external executable: Tridactyl will still work but won't be able to start external programs.
* Read and modify bookmarks:
* Tridactyl's command line has a powerful autocompletion mechanism. In order to be able to autocomplete your bookmarks, Tridactyl needs to read them.
* Clear recent browsing history, cookies, and related data:
* Tridactyl implements the `:sanitise` command Vimperator and Pentadactyl had. It works a bit like the "Clear All History" dialog you can access by pressing `Ctrl+Shift+Del` on default Firefox.
* Get data from the clipboard:
* If your clipboard contains a URL, pressing `p` will make Tridactyl follow this URL in the current tab.
* Input data to the clipboard:
* Tridactyl lets you copy various elements to the clipboard such as a page's URL with `yy`, a link's URL with `;y` or the content of an HTML element with `;p`.
* Download files and read and modify the browser's download history:
* By pressing `;s`, `;S`, `;a` and `;A` you can save documents and pictures from a page to your download folder.
* Access browsing history:
* The URLs of websites you've visited previously can be suggested as arguments for `:tabopen` and similar commands.
* Access recently closed tabs:
* If you've accidentally closed a tab or window, Tridactyl will let you open it again with the `:undo` command which is bound to `u` by default.
* Access browser tabs:
* Tridactyl provides a quick tab-switching menu/command with the `:buffer` command (bound to `b`). This permission is also required to close, move, and pin tabs, amongst other things.
* Access browser activity during navigation:
* This is needed for Tridactyl to be able to go back to normal mode every time you open a new page. In the future we may use it for autocommands.
- Access your data for all websites:
- This is Mozilla's way of saying that Tridactyl can read the content of web pages. This is necessary in order to e.g. find the links you can follow with the `:hint` command (bound to `f` by default).
- Exchange messages with programs other than Firefox
- This permission is required for Tridactyl to interact with your operating system (opening your editor to edit text areas, sending links to your video player, reading a configuration file from your disk...). This is possible thanks to an external executable we provide. If you feel this gives Tridactyl too much power you can chose not to install the external executable: Tridactyl will still work but won't be able to start external programs.
- Read and modify bookmarks:
- Tridactyl's command line has a powerful autocompletion mechanism. In order to be able to autocomplete your bookmarks, Tridactyl needs to read them.
- Clear recent browsing history, cookies, and related data:
- Tridactyl implements the `:sanitise` command Vimperator and Pentadactyl had. It works a bit like the "Clear All History" dialog you can access by pressing `Ctrl+Shift+Del` on default Firefox.
- Get data from the clipboard:
- If your clipboard contains a URL, pressing `p` will make Tridactyl follow this URL in the current tab.
- Input data to the clipboard:
- Tridactyl lets you copy various elements to the clipboard such as a page's URL with `yy`, a link's URL with `;y` or the content of an HTML element with `;p`.
- Download files and read and modify the browser's download history:
- By pressing `;s`, `;S`, `;a` and `;A` you can save documents and pictures from a page to your download folder.
- Access browsing history:
- The URLs of websites you've visited previously can be suggested as arguments for `:tabopen` and similar commands.
- Access recently closed tabs:
- If you've accidentally closed a tab or window, Tridactyl will let you open it again with the `:undo` command which is bound to `u` by default.
- Access browser tabs:
- Tridactyl provides a quick tab-switching menu/command with the `:buffer` command (bound to `b`). This permission is also required to close, move, and pin tabs, amongst other things.
- Access browser activity during navigation:
- This is needed for Tridactyl to be able to go back to normal mode every time you open a new page. In the future we may use it for autocommands.
- Read the text of all open tabs
- This allows us to use Firefox's built-in find-in-page API, for, for example, allowing you to bind find-next and find-previous to `n` and `N`.
- Monitor extension usage and manage themes:
- Tridactyl needs this to integrate with and avoid conflicts with other extensions. For example, Tridactyl's contextual identity features use this to cooperate with the Multi-Account Containers extension.
[betas]: https://tridactyl.cmcaine.co.uk/betas/?sort=time&order=desc

View file

@ -2,16 +2,15 @@
If changing one of these settings fixes your bug, please visit the corresponding Github issue and let us know you encountered the bug.
* `:set noiframeon $URL_OF_THE_WEBSITE` and then reload the page. This disables the Tridactyl commandline on a specific url. [CREATE CORRESPONDING ISSUE]
* `:set allowautofocus true` and then reload the page. This allows website to use the javascript `focus()` function. [#550](https://github.com/cmcaine/tridactyl/issues/550)
* `:set modeindicator false` and then reload the page. This disables the mode indicator. [#821](https://github.com/cmcaine/tridactyl/issues/821)
* `:get csp`. If the value returned is "untouched", try `:set csp clobber`. If the value is "clobber", try `:set csp untouched`. In both cases, please reload the page. This disables (or prevents disabling) some security settings of the page. [#109](https://github.com/cmcaine/tridactyl/issues/109)
- `:seturl $URL_OF_THE_WEBSITE noiframe true` and then reload the page. This disables the Tridactyl commandline on a specific url. [#639](https://github.com/tridactyl/tridactyl/issues/639)
- `:set allowautofocus true` and then reload the page. This allows website to use the javascript `focus()` function. [#550](https://github.com/tridactyl/tridactyl/issues/550)
- `:set modeindicator false` and then reload the page. This disables the mode indicator. [#821](https://github.com/tridactyl/tridactyl/issues/821)
# Native Editor/Messenger issues
If you're having trouble running your editor on OSX, you might be having $PATH issues: [#684](https://github.com/cmcaine/tridactyl/issues/684). The solution is to specify the absolute path to your editor, like this: `:set editorcmd /usr/local/bin/vimr`.
If you're having trouble running your editor on OSX, you might be having \$PATH issues: [#684](https://github.com/tridactyl/tridactyl/issues/684). The solution is to specify the absolute path to your editor, like this: `:set editorcmd /usr/local/bin/vimr`.
If you're encountering problems on windows, you might want to try some of the workarounds mentioned here: [#797](https://github.com/cmcaine/tridactyl/issues/797).
If you're encountering problems on windows, you might want to try some of the workarounds mentioned here: [#797](https://github.com/tridactyl/tridactyl/issues/797).
If you're on Unix, running `printf '%c\0\0\0{"cmd": "run", "command": "echo $PATH"}' 39 | ~/.local/share/tridactyl/native_main.py` in a terminal after you have installed the native messenger will tell you if there are any missing modules.
@ -19,14 +18,14 @@ If you're on Unix, running `printf '%c\0\0\0{"cmd": "run", "command": "echo $PAT
Tridactyl can selectively display logs for certain components. These components are the following:
* messaging
* cmdline
* controller
* containers
* hinting
* state
* styling
* excmds
- messaging
- cmdline
- controller
- containers
- hinting
- state
- styling
- excmds
In order to activate logging for a component, you can use the following command: `:set logging.$COMPONENT DEBUG`. Then, to get the logs, click the hamburger menu in the top right of Firefox, click "Web Developer", then click "Browser Console". Open the menu again and click "Web Console" in the same place.

View file

@ -10,5 +10,5 @@ file_changed() {
}
if file_changed package.json; then
npm install
yarn install
fi

View file

@ -10,5 +10,5 @@ file_changed() {
}
if file_changed package.json; then
npm install
yarn install
fi

View file

@ -1,14 +1,16 @@
#!/usr/bin/env bash
source ./scripts/common
source ./scripts/common.sh
jsfiles=$(cachedFiles)
[ -z "$jsfiles" ] && exit 0
jsfiles=$(cachedTSLintFiles)
otherfiles=$(cachedPrettierFiles)
# Check if any of the files are ugly or contain a console.log call
consoleFiles=$(noisy $jsfiles)
uglyFiles=$(ugly $jsfiles)
uglyFiles="$(tslintUgly $jsfiles)"
if [ ! -n "$uglyFiles" ]; then
uglyFiles="$(prettierUgly $otherfiles)"
fi
if [ -n "$consoleFiles" ]; then
echo "Warning: adding console.log calls in ${consoleFiles[@]}"
@ -18,6 +20,6 @@ fi
if [ -n "$uglyFiles" ]; then
echo "Prettify your files first:"
echo 'npm run pretty'
echo 'yarn run pretty'
exit 1
fi

View file

@ -8,7 +8,7 @@ Please search our `:help` page and through the other issues on this repository;
# Reporting a bug / getting help
If you're opening this issue to report a bug with a specific site, please read and follow the "Settings that can fix websites" paragraph of the (troubleshooting steps)[https://github.com/cmcaine/tridactyl/tree/master/doc/troubleshooting.md] first.
If you're opening this issue to report a bug with a specific site, please read and follow the "Settings that can fix websites" paragraph of the (troubleshooting steps)[https://github.com/tridactyl/tridactyl/tree/master/doc/troubleshooting.md] first.
If that does not solve your problem, please fill in the following template and then delete all the lines above it, and any other lines which you do not feel are applicable:

27
jest.config.js Normal file
View file

@ -0,0 +1,27 @@
const tsConfig = require('./tsconfig');
module.exports = {
preset: "ts-jest",
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
globals: {
"ts-jest": {
tsConfig: {
...tsConfig.compilerOptions,
types: ["jest", "node"]
},
diagnostics: {
ignoreCodes: [151001]
},
}
},
moduleNameMapper: {
"@src/(.*)": "<rootDir>/src/$1"
},
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json"
],
};

View file

@ -3,9 +3,9 @@
set -e
echoerr() {
red="\033[31m"
normal="\e[0m"
echo -e "$red$@$normal" >&2
red="\\033[31m"
normal="\\e[0m"
echo -e "$red$*$normal" >&2
}
sedEscape() {
@ -18,8 +18,10 @@ trap "echoerr 'Failed to install!'" ERR
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/tridactyl"
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/tridactyl"
manifest_loc="https://raw.githubusercontent.com/tridactyl/tridactyl/master/native/tridactyl.json"
native_loc="https://raw.githubusercontent.com/tridactyl/tridactyl/master/native/native_main.py"
# Use argument as version or 1.15.0, as that was the last version before we switched to using tags
manifest_loc="https://raw.githubusercontent.com/tridactyl/tridactyl/${1:-1.15.0}/native/tridactyl.json"
native_loc="https://raw.githubusercontent.com/tridactyl/tridactyl/${1:-1.15.0}/native/native_main.py"
# Decide where to put the manifest based on OS
case "$OSTYPE" in
@ -54,11 +56,21 @@ else
curl -sS --create-dirs -o "$native_file" "$native_loc"
fi
if [[ ! -f "$manifest_file" ]] ; then
echoerr "Failed to create '$manifest_file'. Please make sure that the directories exist and that you have the necessary permissions."
exit 1
fi
if [[ ! -f "$native_file" ]] ; then
echoerr "Failed to create '$native_file'. Please make sure that the directories exist and that you have the necessary permissions."
exit 1
fi
sed -i.bak "s/REPLACE_ME_WITH_SED/$(sedEscape "$native_file_final")/" "$manifest_file"
chmod +x $native_file
chmod +x "$native_file"
# Requirements for native messenger
python_path=$(which python3) || python_path=""
python_path=$(command -v python3) || python_path=""
if [[ -x "$python_path" ]]; then
sed -i.bak "1s/.*/#!$(sedEscape /usr/bin/env) $(sedEscape "$python_path")/" "$native_file"
mv "$native_file" "$native_file_final"

View file

@ -14,7 +14,7 @@ import time
import unicodedata
DEBUG = False
VERSION = "0.1.8"
VERSION = "0.1.11"
class NoConnectionError(Exception):
@ -88,7 +88,7 @@ def findUserConfigFile():
"""
home = os.path.expanduser("~")
config_dir = getenv(
"XDG_CONFIG_HOME", os.path.expanduser("~/.config")
"XDG_CONFIG_HOME", os.path.join(home, ".config")
)
# Will search for files in this order
@ -120,7 +120,7 @@ def getUserConfig():
# for now, this is a simple file read, but if the files can
# include other files, that will need more work
return open(cfg_file, "r").read()
return open(cfg_file, "r", encoding="utf-8").read()
def sanitizeFilename(fn):
@ -413,6 +413,12 @@ def handleMessage(message):
else:
reply["code"] = "File not found"
elif cmd == "getconfigpath":
reply["content"] = findUserConfigFile()
reply["code"] = 0
if reply["content"] is None:
reply["code"] = "Path not found"
elif cmd == "run":
commands = message["command"]
stdin = message.get("content", "").encode("utf-8")
@ -434,7 +440,7 @@ def handleMessage(message):
os.path.expandvars(
os.path.expanduser(message["file"])
),
"r",
"r", encoding="utf-8"
) as file:
reply["content"] = file.read()
reply["code"] = 0
@ -450,18 +456,41 @@ def handleMessage(message):
reply["content"] = ""
reply["code"] = 0
elif cmd == "move":
dest = os.path.expanduser(message["to"])
if (os.path.isfile(dest)):
reply["code"] = 1
else:
try:
shutil.move(os.path.expanduser(message["from"]), dest)
reply["code"] = 0
except Exception:
reply["code"] = 2
elif cmd == "write":
with open(message["file"], "w") as file:
with open(message["file"], "w", encoding="utf-8") as file:
file.write(message["content"])
elif cmd == "writerc":
path = os.path.expanduser(message["file"])
if not os.path.isfile(path) or message["force"]:
try:
with open(path, "w", encoding="utf-8") as file:
file.write(message["content"])
reply["code"] = 0 # Success.
except EnvironmentError:
reply["code"] = 2 # Some OS related error.
else:
reply["code"] = 1 # File exist, send force="true" or try another filename.
elif cmd == "temp":
prefix = message.get("prefix")
if prefix is None:
prefix = ""
prefix = "tmp_{}_".format(sanitizeFilename(prefix))
(handle, filepath) = tempfile.mkstemp(prefix=prefix)
with os.fdopen(handle, "w") as file:
(handle, filepath) = tempfile.mkstemp(prefix=prefix, suffix=".txt")
with os.fdopen(handle, "w", encoding="utf-8") as file:
file.write(message["content"])
reply["content"] = filepath
@ -471,6 +500,16 @@ def handleMessage(message):
elif cmd == "win_firefox_restart":
reply = win_firefox_restart(message)
elif cmd == "list_dir":
path = os.path.expanduser(message.get("path"))
reply["sep"] = os.sep
reply["isDir"] = os.path.isdir(path)
if not reply["isDir"]:
path = os.path.dirname(path)
if not path:
path = "./"
reply["files"] = os.listdir(path)
else:
reply = {"cmd": "error", "error": "Unhandled message"}
eprint("Unhandled message: {}".format(message))

View file

@ -3,5 +3,5 @@
"description": "Tridactyl native command handler",
"path": "REPLACE_ME_WITH_SED",
"type": "stdio",
"allowed_extensions": [ "tridactyl.vim@cmcaine.co.uk","tridactyl.vim.betas@cmcaine.co.uk" ]
"allowed_extensions": [ "tridactyl.vim@cmcaine.co.uk","tridactyl.vim.betas@cmcaine.co.uk", "tridactyl.vim.betas.nonewtab@cmcaine.co.uk" ]
}

View file

@ -2,7 +2,8 @@ Param (
[switch]$Uninstall = $false,
[switch]$NoPython= $false,
[string]$DebugDirBase = "",
[string]$InstallDirBase = ""
[string]$InstallDirBase = "",
[string]$Tag = "1.15.0"
)
#
@ -18,10 +19,8 @@ $global:WinPython3Command = "py -3 -u"
$global:MessengerManifestReplaceStr = "REPLACE_ME_WITH_SED"
$global:PowerShellMinimumVersion = 3
# $git_repo_owner should be "cmcaine" in final release
$git_repo_owner = "cmcaine"
# $git_repo_branch should be "master" in final release
$git_repo_branch = "master"
$git_repo_owner = "tridactyl"
$git_repo_branch = $Tag
$git_repo_proto = "https"
$git_repo_host = "raw.githubusercontent.com"
$git_repo_name = "tridactyl"

14783
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,65 +5,64 @@
"dependencies": {
"@types/css": "0.0.31",
"@types/nearley": "^2.11.0",
"command-line-args": "^5.0.2",
"command-line-args": "^5.1.1",
"csp-serdes": "github:cmcaine/csp-serdes",
"css": "^2.2.4",
"fuse.js": "^3.2.1",
"flyd": "^0.2.8",
"fuse.js": "^3.4.5",
"immer": "^4.0.1",
"mark.js": "^8.11.1",
"mithril": "^2.0.4",
"rss-parser": "^3.7.3",
"semver-compare": "^1.0.0"
},
"devDependencies": {
"@aoberoi/chokidar-cli": "^1.3.0",
"@types/jest": "^23.3.2",
"@types/node": "^10.9.4",
"awesome-typescript-loader": "^5.2.1",
"@types/jest": "^24.0.19",
"@types/mithril": "^2.0.0",
"@types/node": "^12.11.1",
"@types/selenium-webdriver": "^4.0.5",
"cleanslate": "^0.10.1",
"copy-webpack-plugin": "^4.5.2",
"jest": "^23.6.0",
"marked": "^0.5.0",
"nearley": "^2.15.1",
"prettier": "^1.14.2",
"copy-webpack-plugin": "^5.0.4",
"geckodriver": "^1.19.0",
"jest": "^24.9.0",
"marked": "^0.7.0",
"nearley": "^2.19.0",
"prettier": "^1.17.1",
"selenium-webdriver": "^4.0.0-alpha.5",
"shared-git-hooks": "^1.2.1",
"source-map-loader": "^0.2.4",
"ts-jest": "^23.1.4",
"ts-node": "^7.0.1",
"typedoc": "^0.12.0",
"typedoc-default-themes": "git://github.com/glacambre/typedoc-default-themes.git#fix_weird_member_names_bin",
"typescript": "^3.0.3",
"ts-jest": "^24.1.0",
"ts-loader": "^6.2.0",
"ts-node": "^8.4.1",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"tslint": "^5.20.0",
"tslint-etc": "^1.7.0",
"tslint-sonarts": "^1.9.0",
"typedoc": "^0.15.0",
"typedoc-default-themes": "git://github.com/tridactyl/typedoc-default-themes.git#fix_weird_member_names_bin",
"typescript": "^3.6.4",
"uglify-es": "^3.3.9",
"uglifyjs-webpack-plugin": "^1.3.0",
"web-ext": "^2.9.1",
"web-ext-types": "github:kelseasy/web-ext-types",
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0"
"uglifyjs-webpack-plugin": "^2.2.0",
"web-ext": "^3.2.0",
"web-ext-types": "^3.2.1",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9"
},
"scripts": {
"build": "sh scripts/build.sh",
"run": "web-ext run -s build/ -u 'txti.es'",
"watch": "echo 'watch is broken, use build instead'; exit 0; chokidar src scripts --initial --silent -i 'src/excmds_{background,content}.ts' -i 'src/static/docs' -c 'npm run build'",
"forrest-run": "yarn run run",
"watch": "echo 'watch is broken, use build instead'; exit 0;",
"clean": "rm -rf build generated",
"test": "npm run build && jest --silent",
"update-buildsystem": "rm -rf src/node_modules; npm run clean",
"test": "yarn run build && rm -rf web-ext-artifacts/* && web-ext build --source-dir ./build --overwrite-dest && mv web-ext-artifacts/*.zip web-ext-artifacts/tridactyl.xpi && jest --silent",
"update-buildsystem": "rm -rf src/node_modules; yarn run clean",
"lint": "bash hooks/pre-commit",
"pretty": "bash scripts/pretty"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
]
"pretty": "bash scripts/pretty.sh"
},
"author": "Colin Caine",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/cmcaine/tridactyl.git"
"url": "git+ssh://git@github.com/tridactyl/tridactyl.git"
},
"keywords": [
"webextension",
@ -73,7 +72,7 @@
],
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/cmcaine/tridactyl/issues"
"url": "https://github.com/tridactyl/tridactyl/issues"
},
"homepage": "https://github.com/cmcaine/tridactyl#readme"
"homepage": "https://github.com/tridactyl/tridactyl#readme"
}

116
readme.md
View file

@ -8,7 +8,7 @@ Replace Firefox's default control mechanism with one modelled on the one true ed
## Installing
[Simply click this link in Firefox to install our latest "beta" build][riskyclick]. These [betas][betas] are updated with each commit to master on this repo. Your browser will automatically update from there once a day. If you want more frequent updates, you can change `extensions.update.interval` in `about:config` to whatever time you want, say, 15 minutes (900 seconds). Alternatively, you can get our "stable" builds straight from [Mozilla][amo]. The changelog for the stable versions can be found [here](https://github.com/cmcaine/tridactyl/blob/master/CHANGELOG.md). If you want to use advanced features such as edit-in-Vim, you'll also need to install the native messenger or executable, instructions for which can be found by typing `:installnative` and hitting enter once you are in Tridactyl.
[Simply click this link in Firefox to install our latest "beta" build][riskyclick]. These [betas][betas] are updated with each commit to master on this repo. Your browser will automatically update from there once a day. If you want more frequent updates, you can change `extensions.update.interval` in `about:config` to whatever time you want, say, 15 minutes (900 seconds). Alternatively, you can get our "stable" builds straight from the Arch Linux community repository: Arch users should just run `pacman -S firefox-tridactyl` in a terminal and then restart Firefox twice; everyone else can install manually from [an Arch mirror here](https://archive.archlinux.org/packages/f/firefox-tridactyl/firefox-tridactyl-1.16.3-1-any.pkg.tar.xz): extract the XPI from that archive and then open it with Firefox. The changelog for the stable versions can be found [here](https://github.com/tridactyl/tridactyl/blob/master/CHANGELOG.md). There is also another beta build that comes without a new tab page. You can get it from [here][nonewtablink]. If you want to use advanced features such as edit-in-Vim, you'll also need to install the native messenger or executable, instructions for which can be found by typing `:installnative` and hitting enter once you are in Tridactyl. Arch users can install the [AUR package](https://aur.archlinux.org/packages/firefox-tridactyl-native/) `firefox-tridactyl-native` instead. To migrate your configuration across builds, see [this comment][migratelink] or [this issue](https://github.com/tridactyl/tridactyl/issues/1353#issuecomment-463094704).
Type `:help` or press `<F1>` for online help once you're in :)
@ -39,7 +39,7 @@ You can try `:help key` to know more about `key`. If it is an existing binding,
- `h`/`l` — scroll left/right
- `^`/`$` — scroll to left/right margin
- `gg`/`G` — scroll to start/end of page
- `f`/`F` — enter "hint mode" to select a link to follow. `F` to open in a background tab (note: hint characters should be typed in lowercase)
- `f`/`F`/`gF` — enter "hint mode" to select a link to follow. `F` to open in a background tab (note: hint characters should be typed in lowercase). `gF` to repeatedly open links until you hit `<Escape>`.
- `gi` — scroll to and focus the last-used input on the page
- `r`/`R` — reload page or hard reload page
- `yy` — copy the current page URL to the clipboard
@ -49,13 +49,17 @@ You can try `:help key` to know more about `key`. If it is an existing binding,
- `gU` — go to the root domain of the current URL
- `gr` — open Firefox reader mode (note: Tridactyl will not work in this mode)
- `zi`/`zo`/`zz` — zoom in/out/reset zoom
- `<C-f>`/`<C-b>` — jump to the next/previous part of the page
- `g?` — Apply Caesar cipher to page (run `g?` again to switch back)
#### Find mode
Find mode is still incomplete and uses the built-in Firefox search. This will be improved eventually.
Find mode is still incomplete and uses the Firefox feature "Quick Find". This will be improved eventually.
- `/` — open the find search box
- `C-g`/`C-G` — find the next/previous instance of the last find operation (note: these are the standard Firefox shortcuts)
- `<C-g>`/`<C-G>` — find the next/previous instance of the last find operation (note: these are the standard Firefox shortcuts)
Please note that Tridactyl overrides Firefox's `<C-f>` search, replacing it with a binding to go to the next part of the page. If you want to be able to use `<C-f>` again to search for things, use `unbind <C-f>`.
#### Bookmarks and quickmarks
@ -64,6 +68,8 @@ Find mode is still incomplete and uses the built-in Firefox search. This will be
- `M<key>` — bind a quickmark to the given key
- `go<key>`/`gn<key>`/`gw<key>` — open a given quickmark in current tab/new tab/new window
If you want to use Firefox's default `<C-b>` binding to open the bookmarks sidebar, make sure to run `unbind <C-b>` because Tridactyl replaces this setting with one to go to the previous part of the page.
#### Navigating to new pages:
- `o`/`O` — open a URL (or default search) in this tab (`O` to pre-load current URL)
@ -106,17 +112,17 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
## WebExtension-related issues
- Navigation to any about:\* pages using `:open` requires the native messenger.
- Firefox will not load Tridactyl on about:\*, some file:\* URIs, view-source:\*, or data:\*. On these pages Ctrl-L (or F6), Ctrl-Tab and Ctrl-W are your escape hatches.
- Firefox will not load Tridactyl on about:\*, some file:\* URIs, view-source:\*, or data:\*. On these pages Ctrl-L (or F6), Ctrl-Tab, Ctrl-W, and the `tri` [omnibox keyword](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Omnibox) are your escape hatches.
- addons.mozilla.org is now supported so long as you run `fixamo` first.
- Tridactyl now supports changing the Firefox GUI if you have the native messenger installed via `guiset`. There's quite a few options available, but `guiset gui none` is probably what you want, perhaps followed up with `guiset tabs always`.
- Tridactyl now supports changing the Firefox GUI if you have the native messenger installed via `guiset`. There's quite a few options available, but `guiset gui none` is probably what you want, perhaps followed up with `guiset tabs always`. See `:help guiset` for a list of all possible options.
## Frequently asked questions
- Why doesn't Tridactyl respect my search engine settings?
It's a webextension limitation. Firefox doesn't allow reading user preferences.
It used to be a webextension limitation but it's not anymore. There are plans to fix this, see [#792](https://github.com/tridactyl/tridactyl/issues/792).
- Why doesn't Tridactyl work?
- Why doesn't Tridactyl work/why does it break the websites I'm trying to use? or 'Help! A website I use is totally blank when I try to use it with Tridactyl enabled!' or 'Why doesn't Tridactyl work on some pages?'
Please visit our [troubleshooting guide](https://github.com/tridactyl/tridactyl/blob/master/doc/troubleshooting.md).
@ -136,53 +142,41 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
- Can I import/export settings, and does Tridactyl use an external configuration file just like Vimperator?
Yes, if you have `native` working, `$XDG_CONFIG_DIR/tridactyl/tridactylrc` or `~/.tridactylrc` will be read at startup via an `autocmd` and `source`. There is an [example file available on our repository](https://github.com/cmcaine/tridactyl/blob/master/.tridactylrc).
Yes, if you have `native` working, `$XDG_CONFIG_DIR/tridactyl/tridactylrc` or `~/.tridactylrc` will be read at startup via an `autocmd` and `source`. There is an [example file available on our repository](https://github.com/tridactyl/tridactyl/blob/master/.tridactylrc).
If you can't use the native messenger for some reason, there is a workaround: if you do `set storageloc local`, a JSON file will appear at `<your firefox profile>\browser-extension-data\tridactyl.vim@cmcaine.co.uk\storage.js`. You can find your profile folder by going to `about:support`. You can edit this file to your heart's content.
- How can I change the colors or theme used by Tridactyl?
- I hate the light, can I get a dark theme/dark mode?
Use `:colors dark` (authored by @furgerf), `:colors shydactyl` (authored by @atrnh) or `:colors greenmat` (authored by @caputchinefrobles). Tridactyl can also load themes from disk, which would let you use one of the themes authored by @bezmi ([bezmi/base16-tridactyl](https://github.com/bezmi/base16-tridactyl)), see `:help colors` for more information.
Yes: `set theme dark` or `colors dark`. Thanks to @fugerf.
- How to remap keybindings? or How can I bind keys using the control/alt key modifiers (eg: `ctrl+^`)?
- How can I pretend that I'm not a 1337 h4x0r?
You can remap keys in normal, ignore, input and insert mode with `:bind --mode=$mode $key $excmd`. Hint mode and the command line are currently special and can't be rebound. See `:help bind` for more information.
We cater for you, too! `set theme shydactyl`. Thanks to @atrnh.
- How can I pretend that I'm a 1337 h4x0r?
We cater for you, too! `set theme greenmat`. Thanks to @caputchinefrobles.
- How can I bind keys using the control/alt key modifiers (eg: `ctrl+^`)?
`:bind <C-f> scrollpage 1`. Special keys can be bound too: `:bind <F3> set theme dark` and with modifiers: `:bind <S-F3> set theme default` and with multiple modifiers: `:bind <SA-F3> composite set hintchars 1234567890 | set hintfiltermode vimperator-reflow`
Modifiers can be bound like this: `:bind <C-f> scrollpage 1`. Special keys can be bound too: `:bind <F3> colors dark` and with modifiers: `:bind <S-F3> colors default` and with multiple modifiers: `:bind <SA-F3> composite set hintchars 1234567890 | set hintfiltermode vimperator-reflow`
The modifiers are case insensitive. Special key names are not. The names used are those reported by Javascript with a limited number of vim compatibility aliases (e.g. `CR == Enter`).
If you want to bind <C-^> you'll find that you'll probably need to press Control+Shift+6 to trigger it. The default bind is <C-6> which does not require you to press shift.
- How can I tab complete from bookmarks?
`bmarks`. Bookmarks are not currently supported on `*open`: see [issue #214](https://github.com/cmcaine/tridactyl/issues/214).
- When I type 'f', can I type link names (like Vimperator) in order to narrow down the number of highlighted links?
You can, thanks to @saulrh. First `set hintfiltermode vimperator` and then `set hintchars 1234567890`.
- How to remap keybindings in both normal mode and ex mode?
You cannot. We only support normal mode bindings for now, with `bind [key] [excmd]`
- Where can I find a changelog for the different versions (to see what is new in the latest version)?
[Here.](https://github.com/cmcaine/tridactyl/blob/master/CHANGELOG.md)
[Here.](https://github.com/tridactyl/tridactyl/blob/master/CHANGELOG.md)
- Why can't I use my bookmark keywords?
Mozilla doesn't give us access to them. See [issue #73](https://github.com/cmcaine/tridactyl/issues/73).
Mozilla doesn't give us access to them. See [issue #73](https://github.com/tridactyl/tridactyl/issues/73).
- Can I set/get my bookmark tags from Tridactyl?
No, Mozilla doesn't give us access to them either.
- Why doesn't Tridactyl work on websites with frames?
It should work on some frames now. See [#122](https://github.com/cmcaine/tridactyl/issues/122).
It should work on some frames now. See [#122](https://github.com/tridactyl/tridactyl/issues/122).
- Can I change proxy via commands?
@ -190,46 +184,52 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
- How do I disable Tridactyl on certain sites?
In the beta you can use `blacklistadd`, like this: `blacklistadd mail.google.com/mail`.
You can use `blacklistadd`, like this: `blacklistadd mail.google.com/mail`. See `:help blacklistadd`. Also note that if you want something like the passkeys or ignorekeys features vimperator/pentadactyl had, you can use `bindurl`. See `:help bindurl`.
- How can I list the current bindings?
`viewconfig nmaps` works OK, but Tridactyl commands won't work on the shown page for "security reasons". We'll eventually provide a better way. See [#98](https://github.com/cmcaine/tridactyl/issues/98).
- Why doesn't Tridactyl work on some pages?
One possible reason is that the site has a strict content security policy. You can try to use `set csp clobber` to fix this, but know that it could worsen the security of sensitive pages.
`viewconfig nmaps` works OK, but Tridactyl commands won't work on the shown page for "security reasons". We'll eventually provide a better way. See [#98](https://github.com/tridactyl/tridactyl/issues/98).
- How can I know which mode I'm in/have a status line?
Press `j` and see if you scroll down :) There's no status line yet: see [#210](https://github.com/cmcaine/tridactyl/issues/210), but we do have a "mode indicator" in the bottom right. It even goes purple when you're in a private window :).
Press `j` and see if you scroll down :) There's no status line yet: see [#210](https://github.com/tridactyl/tridactyl/issues/210), but we do have a "mode indicator" in the bottom right. It even goes purple when you're in a private window :).
- Does anyone actually use Tridactyl?
In addition to the developers, some other people do. Mozilla keeps tabs on them [here](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/statistics/?last=30).
In addition to the developers, some other people do. Mozilla keeps tabs on stable users [here](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/statistics/?last=30). The maintainers guess the number of unstable users from unique IPs downloading the betas each week when they feel like it. Last time they checked there were 3000 of them.
- How do I prevent websites from stealing focus?
There are two ways to do that, the first one is `set allowautofocus false` (if you do this you'll probably also want to set `browser.autofocus` to false in `about:config`). This will prevent the page's `focus()` function from working and could break javascript text editors such as Ace or CodeMirror. Another solution is to use `autocmd TabEnter .* unfocus` in the beta, JS text editors should still work but pages won't steal focus when entering their tabs anymore.
- Help! A website I use is totally blank when I try to use it with Tridactyl enabled!
Try `set noiframeon [space separated list of URLs to match]`. If that doesn't work, please file an issue.
## Contributing
### Donations
We gratefully accept donations via [GitHub Sponsors](https://github.com/users/bovine3dom/sponsorship) (who will double any donations until October 2020), [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY) and [Patreon](https://www.patreon.com/tridactyl). If you can, please make this a monthly donation as it makes it much easier to plan.
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY"><img src="https://www.paypalobjects.com/en_US/GB/i/btn/btn_donateCC_LG.gif" alt="PayPal"></a>
Funds will be used at the discretion of the main contributors (currently bovine3dom, cmcaine, glacambre and antonva) for Tridactyl-related expenditure, such as domain names, server costs, small thank-yous to contributors such as stickers, and victuals for hackathons.
### Merchandise
We have some designs available on [REDBUBBLE](https://www.redbubble.com/people/bovine3dom/shop/top+selling?ref=artist_shop_category_refinement&asc=u). There are often discount codes available - just search your favourite search engine for them. The T-shirts are quite good (I'm wearing one as I type this). The stickers are not the best quality we've ever seen. The clock looks amazing on the website. If anyone buys it I would love to see it.
**We don't take any cut from the merchandise**, so if you would like to donate, please do so via PayPal or Patreon above.
### Building and installing
Onboarding:
```
git clone https://github.com/cmcaine/tridactyl.git
git clone https://github.com/tridactyl/tridactyl.git
cd tridactyl
npm install
npm run build
yarn install
yarn run build
```
Each time package.json or package-lock.json change after you checkout or pull, you should run `npm install` again.
Each time package.json or package-lock.json change after you checkout or pull, you should run `yarn install` again.
Addon is built in tridactyl/build. Load it as a temporary addon in firefox with `about:debugging` or see [Development loop](#Development-loop). The addon should work in Firefox 52+, but we're only deliberately supporting >=57.
@ -237,13 +237,17 @@ If you want to install a local copy of the add-on into your developer or nightly
```
# Build tridactyl if you haven't done that yet
npm run build
yarn run build
# Package for a browser
$(npm bin)/web-ext build -s build
"$(yarn bin)/web-ext" build -s build
```
If you want to build a signed copy (e.g. for the non-developer release), you can do that with `web-ext sign`. You'll need some keys for AMO and to edit the application id in `src/manifest.json`. There's a helper script in `scripts/sign` that's used by our build bot and for manual releases.
You can build unsigned copies with `scripts/sign nosign{stable,beta}`. NB: The `stable` versus `beta` part of the argument tells our build process which extension ID to use (and therefore which settings to use). If you want a stable build, make sure you are on the latest tag, i.e. `git checkout $(git tag | grep '^[0-9]\+\.[0-9]\+\.[0-9]\+$' | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -1)`.
If you are on a distribution which builds Firefox with `--with-unsigned-addon-scopes=` set to `app` and/or `system` (which is most of them by users: Arch, Debian, Ubuntu), you can install your unsigned copy of Tridactyl with `scripts/install.sh [directory]`. If you're on Arch, the correct directory is probably selected by default; on other distributions you might have to go hunting, but it probably looks like `/usr/lib/firefox/browser/extensions`.
### Building on Windows
- Install [Git for Windows][win-git]
@ -322,22 +326,22 @@ PS C:\Users\{USERNAME}\.tridactyl> gpg2 --verify .\native_main.exe.sig .\native_
### Development loop
```
npm run build & npm run run
yarn run build & yarn run run
```
<!-- This will compile and deploy your files each time you save them. -->
You'll need to run `npm run build` every time you edit the files, and press "r" in the `npm run run` window to make sure that the files are properly reloaded.
You'll need to run `yarn run build` every time you edit the files, and press "r" in the `yarn run run` window to make sure that the files are properly reloaded.
### Committing
A pre-commit hook is added by `npm install` that simply runs `npm test`. If you know that your commit doesn't break the tests you can commit with `git commit -n` to ignore the hooks. If you're making a PR, travis will check your build anyway.
A pre-commit hook is added by `yarn install` that simply runs `yarn test`. If you know that your commit doesn't break the tests you can commit with `git commit -n` to ignore the hooks. If you're making a PR, travis will check your build anyway.
### Documentation
Ask in `#tridactyl` on [matrix.org][matrix-link], freenode, or [gitter][gitter-link]. We're friendly!
Default keybindings are currently best discovered by reading the [default config](./src/config.ts).
Default keybindings are currently best discovered by reading the [default config](./src/lib/config.ts).
Development notes are in the doc directory, but they're mostly out of date now. Code is quite short and not _too_ badly commented, though.
@ -368,4 +372,6 @@ The logo was designed by Jake Beazley using free vector art by <a target="_blank
[matrix-link]: https://riot.im/app/#/room/#tridactyl:matrix.org
[betas]: https://tridactyl.cmcaine.co.uk/betas/?sort=time&order=desc
[riskyclick]: https://tridactyl.cmcaine.co.uk/betas/tridactyl-latest.xpi
[nonewtablink]: https://tridactyl.cmcaine.co.uk/betas/nonewtab/tridactyl_no_new_tab_beta-latest.xpi
[amo]: https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim?src=external-github
[migratelink]: https://github.com/tridactyl/tridactyl/issues/79#issuecomment-351132451

View file

@ -3,4 +3,4 @@
# Put the AMO flavour text in your clipboard for easy pasting.
# AMO doesn't support all HTML in markdown so we strip it out.
$(npm bin)/marked doc/amo.md | sed -r "s/<.?p>//g" | sed -r "s/<.?h.*>//g" | xclip -selection "clipboard"
"$(yarn bin)/marked" doc/amo.md | sed -r "s/<.?p>//g" | sed -r "s/<.?h.*>//g" | xclip -selection "clipboard"

View file

@ -1,9 +1,10 @@
#!/usr/bin/env bash
imports=$(find src/static/themes/ -name '*.css'| sed "s/^src\/static\///" | sed "s/^.*$/@import url\('..\/\0'\);/")
set -e
shopt -s globstar
imports=$(find src/static/themes -name '*.css'| awk -F"/" '{ printf "@import url('\''../%s/%s/%s'\'');\n", $3, $4, $5 }')
for css in $(ls build/static/css/**/*.css); do
printf '%s\n%s\n' "$imports" "$(cat $css)" > $css
for css in build/static/css/*.css; do
printf '%s\n%s\n' "$imports" "$(cat "$css")" > "$css"
done

View file

@ -3,23 +3,24 @@
set -e
CLEANSLATE="node_modules/cleanslate/docs/files/cleanslate.css"
TRIDACTYL_LOGO="src/static/logo/Tridactyl_64px.png"
isWindowsMinGW() {
local is_mingw="False"
is_mingw="False"
if [ "$(uname | cut -c 1-5)" = "MINGW" ] \
|| [ "$(uname | cut -c 1-4)" = "MSYS" ]; then
is_mingw="True"
fi
echo -n "${is_mingw}"
printf "%s" "${is_mingw}"
}
if [ "$(isWindowsMinGW)" = "True" ]; then
WIN_PYTHON="py -3"
NPM_BIN_DIR="$(cygpath $(npm bin))"
PATH=$NPM_BIN_DIR:$PATH
YARN_BIN_DIR="$(cygpath "$(yarn bin)")"
PATH=$YARN_BIN_DIR:$PATH
else
PATH="$(npm bin):$PATH"
PATH="$(yarn bin):$PATH"
fi
export PATH
@ -35,35 +36,36 @@ else
scripts/excmds_macros.py
fi
# .bracketexpr.generated.ts is needed for metadata generation
"$(yarn bin)/nearleyc" src/grammars/bracketexpr.ne > \
src/grammars/.bracketexpr.generated.ts
# It's important to generate the metadata before the documentation because
# missing imports might break documentation generation on clean builds
"$(npm bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2016 \
"$(yarn bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2017 \
&& node compiler/gen_metadata.js \
--out src/.metadata.generated.ts \
--themeDir src/static/themes \
src/excmds.ts src/config.ts
src/excmds.ts src/lib/config.ts
scripts/newtab.md.sh
scripts/make_tutorial.sh
scripts/make_docs.sh &
scripts/make_docs.sh
$(npm bin)/nearleyc src/grammars/bracketexpr.ne \
> src/grammars/.bracketexpr.generated.ts
if [ "$(isWindowsMinGW)" = "True" ]; then
powershell \
-NoProfile \
-InputFormat None \
-ExecutionPolicy Bypass \
native/win_install.ps1 -DebugDirBase native
else
native/install.sh local
if [ "$1" != "--no-native" ]; then
if [ "$(isWindowsMinGW)" = "True" ]; then
powershell \
-NoProfile \
-InputFormat None \
-ExecutionPolicy Bypass \
native/win_install.ps1 -DebugDirBase native
else
native/install.sh local
fi
fi
(webpack --display errors-only \
&& scripts/git_version.sh) &
wait
(webpack --display errors-only --bail\
&& scripts/git_version.sh)
scripts/bodgecss.sh
scripts/authors.sh
@ -71,5 +73,16 @@ scripts/authors.sh
if [ -e "$CLEANSLATE" ] ; then
cp -v "$CLEANSLATE" build/static/css/cleanslate.css
else
echo "Couldn't find cleanslate.css. Try running 'npm install'"
echo "Couldn't find cleanslate.css. Try running 'yarn install'"
fi
if [ -e "$TRIDACTYL_LOGO" ] ; then
# sed and base64 take different arguments on Mac
case "$(uname)" in
Darwin*) sed -i "" "s@REPLACE_ME_WITH_BASE64_TRIDACTYL_LOGO@$(base64 "$TRIDACTYL_LOGO")@" build/static/themes/default/default.css;;
*BSD) sed -in "s@REPLACE_ME_WITH_BASE64_TRIDACTYL_LOGO@$(base64 "$TRIDACTYL_LOGO" | tr -d '\r\n')@" build/static/themes/default/default.css;;
*) sed "s@REPLACE_ME_WITH_BASE64_TRIDACTYL_LOGO@$(base64 --wrap 0 "$TRIDACTYL_LOGO")@" -i build/static/themes/default/default.css;;
esac
else
echo "Couldn't find Tridactyl logo ($TRIDACTYL_LOGO)"
fi

4
scripts/changelog_mangler.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
# Replace issue numbers in brackets eg (#1337) with a link to the issue on the repository
sed -i 's; (#\([0-9]*\)); ([#\1](https://github.com/tridactyl/tridactyl/issues/\1));g' CHANGELOG.md

View file

@ -1,34 +0,0 @@
#!/usr/bin/env bash
# Accepts no arguments
# Returns git-add'ed files as a list of filenames separated by a newline character
cachedFiles() {
git diff --cached --name-only --diff-filter=ACM "*.js" "*.jsx" "*.ts" "*.tsx" "*.md" "*.css"
}
# Accepts a single argument which is the name of a file tracked by git
# Returns a string which is the content of the file as stored in the git index
staged() {
git show :"$1"
}
# Accepts a single string argument made of multiple file names separated by a newline
# Returns an array of files that prettier wants to lint
ugly() {
local acc=""
local IFS=$'\n'
for jsfile in $1; do
diff <(staged "$jsfile") <(staged "$jsfile" | "$(npm bin)/prettier" --stdin-filepath "$jsfile") >/dev/null || acc="$jsfile"$'\n'"$acc"
done
echo "$acc"
}
noisy() {
local acc=()
for jsfile in "$@"; do
if [ "$(git diff --cached "$jsfile" | grep '^+.*console.log' -c)" -gt '0' ] ; then
acc+=("jsfile")
fi
done
echo ${acc[@]}
}

55
scripts/common.sh Executable file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Accepts no arguments
# Returns git-add'ed files as a list of filenames separated by a newline character
cachedTSLintFiles() {
git diff --cached --name-only --diff-filter=ACM "*.js" "*.jsx" "*.ts" "*.tsx" ":(exclude)*.d.ts" ":(exclude)tests/*" ":(exclude)*test.ts"
}
# Accepts no arguments
# Returns git-add'ed files as a list of filenames separated by a newline character
cachedPrettierFiles() {
git diff --cached --name-only --diff-filter=ACM "*.md" "*.css"
}
# Accepts a single argument which is the name of a file tracked by git
# Returns a string which is the content of the file as stored in the git index
staged() {
git show :"$1"
}
# Accepts a single string argument made of multiple file names separated by a newline
# Returns an array of files that prettier wants to lint
prettierUgly() {
local acc=""
local IFS=$'\n'
for jsfile in $1; do
diff <(staged "$jsfile") <(staged "$jsfile" | "$(yarn bin)/prettier" --stdin-filepath "$jsfile") >/dev/null || acc="$jsfile"$'\n'"$acc"
done
echo "$acc"
}
tslintUgly() {
local acc=""
local IFS=$'\n'
local tmpdir
tmpdir=$(mktemp -d "tslint.XXXXXXXXX")
for jsfile in "$@"; do
tmpfile="$tmpdir/$jsfile"
mkdir -p "$(dirname "$tmpfile")"
staged "$jsfile" > "$tmpfile"
tslint -q "$tmpfile" 2>/dev/null || acc="$jsfile"$'\n'"$acc"
done
rm -rf "$tmpdir"
echo "$acc"
}
noisy() {
local acc=()
for jsfile in "$@"; do
if [ "$(git diff --cached "$jsfile" | grep '^+.*console.log' -c)" -gt '0' ] ; then
acc+=("jsfile")
fi
done
echo "${acc[@]}"
}

View file

@ -17,6 +17,7 @@ Caveats:
from collections import OrderedDict
import re
import textwrap
class Signature:
"""Extract name, parameters and types from a function signature."""
@ -84,36 +85,69 @@ def dict_to_js(d):
"Py dict to string that when eval'd will produce equivalent js Map"
return "new Map(" + str(list(d.items())).replace('(','[').replace(')',']') + ")"
def content(lines, context):
"Extract params and replace function with a shim if context==background."
"Extract signature and, if context==background, replace function with a shim."
block = get_block(lines)
sig = Signature(block.split('\n')[0])
cmd_params = "cmd_params.set('{sig.name}', ".format(**locals()) + dict_to_js(sig.params) + ")"
message_params = ", ".join(sig.params.keys())
if context == "background":
# Consume and replace this block.
block = get_block(lines)
sig = Signature(block.split('\n')[0])
return "cmd_params.set('{sig.name}', ".format(**locals()) + dict_to_js(sig.params) + """)
{sig.raw}
return messageActiveTab(
"excmd_content",
"{sig.name}",
""".format(**locals()) + str(list(sig.params.keys())).replace("'","") + """,
)
}\n"""
# Consume and replace this block. We emit the line to add the
# function's signature to cmd_params, the function's signature
# line unchanged, then a command to message the browser's
# active tab forwarding all parameters.
return textwrap.dedent("""\
{cmd_params}
{sig.raw}
logger.debug("shimming excmd {sig.name} from background to content")
return Messaging.messageActiveTab(
"excmd_content",
"{sig.name}",
[{message_params}],
)
}}\n""".format(**locals()))
else:
# Do nothing
return ""
# Emit the line to add the function to cmd_params, then
# re-emit the original block (because we consumed the block so
# we could compute the cmd params)
return "{cmd_params}\n{block}".format(**locals())
def background(lines, context):
"Extract params if context is background, else omit"
"Extract signature and, if context==content, replace function with a shim."
block = get_block(lines)
sig = Signature(block.split('\n')[0])
cmd_params = "cmd_params.set('{sig.name}', ".format(**locals()) + dict_to_js(sig.params) + ")"
message_params = ", ".join(sig.params.keys())
if context == "background":
sig = Signature(next(lines))
return "cmd_params.set('{sig.name}', ".format(**locals()) + dict_to_js(sig.params) + """)
{sig.raw}""".format(**locals())
# Emit the line to add the function to cmd_params, then
# re-emit the original block (because we consumed the block so
# we could compute the cmd params)
return "{cmd_params}\n{block}".format(**locals())
else:
# Omit
get_block(lines)
return ""
# Consume and replace this block. We emit the line to add the
# function's signature to cmd_params, the function's signature
# line unchanged, then a command to message the browser's
# active tab forwarding all parameters.
return textwrap.dedent("""\
{cmd_params}
{sig.raw}
logger.debug("shimming excmd {sig.name} from content to background")
return Messaging.message(
"excmd_background",
"{sig.name}",
[{message_params}],
)
}}\n""".format(**locals()))
def both(lines, context):
"Just extract the signature of the command."
sig = Signature(next(lines))
return "cmd_params.set('{sig.name}', ".format(**locals()) + dict_to_js(sig.params) + """)\n{sig.raw}""".format(**locals())
def omit_helper_func_factory(desired_context):
@ -142,6 +176,7 @@ def main():
macros = {
"content": content,
"background": background,
"both": both,
"content_helper": omit_helper_func_factory("content"),
"background_helper": omit_helper_func_factory("background"),
"content_omit_line": omit_line_factory("content"),
@ -149,7 +184,7 @@ def main():
}
for context in ("background", "content"):
with open("src/excmds.ts") as source:
with open("src/excmds.ts", encoding="utf-8") as source:
output = PRELUDE
lines = iter(source)
for line in lines:
@ -162,7 +197,7 @@ def main():
else:
output += line
# print(output.rstrip())
with open("src/.excmds_{context}.generated.ts".format(**locals()), "w") as sink:
with open("src/.excmds_{context}.generated.ts".format(**locals()), "w", encoding="utf-8") as sink:
print(output.rstrip(), file=sink)

4
scripts/get_id_from_xpi.sh Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
temp=$(mktemp -d)
unzip -qq "$1" -d "$temp"
jq '.applications.gecko.id' "$temp"/manifest.json | tr -d '"'

View file

@ -1,10 +1,14 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
gitversion=$(git describe --tags | cut -d"-" -f2-)
gitversion=pre$(git rev-list --count HEAD)-$(git rev-parse --short HEAD)
if grep -Fq 'tridactyl.vim@cmcaine' ./src/manifest.json ; then
gitversion=""
fi
manversion=$(grep '"version":' ./src/manifest.json | cut -d":" -f2 | tr -d \" | tr -d , | cut -d" " -f2)
version=$manversion-$gitversion
version=$manversion$gitversion
sed -i.bak 's/REPLACE_ME_WITH_THE_VERSION_USING_SED/'$version'/' ./build/background.js
sed -i.bak 's/REPLACE_ME_WITH_THE_VERSION_USING_SED/'$version'/' ./build/static/newtab.html
sed -i.bak 's/REPLACE_ME_WITH_THE_VERSION_USING_SED/'"$version"'/' ./build/background.js
sed -i.bak 's/REPLACE_ME_WITH_THE_VERSION_USING_SED/'"$version"'/' ./build/content.js
sed -i.bak 's/REPLACE_ME_WITH_THE_VERSION_USING_SED/'"$version"'/' ./build/static/newtab.html
rm ./build/background.js.bak
rm ./build/static/newtab.html.bak

4
scripts/install.sh Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
installdir=${1:-/usr/lib/firefox/browser/extensions}
xpi=web-ext-artifacts/$(ls -t web-ext-artifacts/ | head -n1)
install -Dm644 "$xpi" "$installdir"/"$(scripts/get_id_from_xpi.sh "$xpi")".xpi

View file

@ -1,4 +1,4 @@
#!/bin/sh
dest=generated/static/docs
$(npm bin)/typedoc --theme src/static/typedoc/ --out $dest src --ignoreCompilerErrors
"$(yarn bin)/typedoc" --theme src/static/typedoc/ --exclude "src/**/?(test_utils|*.test).ts" --out $dest src --ignoreCompilerErrors
cp -r $dest build/static/

View file

@ -2,15 +2,18 @@
# Combine tutorial markdown and template
cd src/static/clippy
if ! cd src/static/clippy; then
echo "Failed to cd in src/static/clippy. Aborting."
exit
fi
pages=$(ls *.md)
pages=$(ls ./*.md)
dest="../../../generated/static/clippy/"
for page in $pages
do
fileroot=$(echo $page | cut -d'.' -f-1)
fileroot=$(echo "$page" | cut -d'.' -f-2)
sed "/REPLACETHIS/,$ d" tutor.template.html > "$dest$fileroot.html"
$(npm bin)/marked $page >> "$dest$fileroot.html"
"$(yarn bin)/marked" "$page" >> "$dest$fileroot.html"
sed "1,/REPLACETHIS/ d" tutor.template.html >> "$dest$fileroot.html"
done

View file

@ -2,28 +2,33 @@
# Combine newtab markdown and template
cd src/static
if ! cd src/static ; then
echo "Failed to cd in src/static. Aborting."
exit
fi
newtab="../../generated/static/newtab.html"
newtabtemp="../../generated/static/newtab.temp.html"
sed "/REPLACETHIS/,$ d" newtab.template.html > "$newtabtemp"
$(npm bin)/marked newtab.md >> "$newtabtemp"
"$(yarn bin)/marked" newtab.md >> "$newtabtemp"
sed "1,/REPLACETHIS/ d" newtab.template.html >> "$newtabtemp"
# Why think when you can pattern match?
sed "/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/,$ d" "$newtabtemp" > "$newtab"
(
sed "/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/,$ d" "$newtabtemp"
# Note: If you're going to change this HTML, make sure you don't break the JS in src/newtab.ts
echo """
cat <<EOF
<input type="checkbox" id="spoilerbutton" />
<label for="spoilerbutton" onclick=""><div id="nagbar-changelog">New features!</div>Changelog</label>
<div id="changelog" class="spoiler">
""" >> "$newtab"
$(npm bin)/marked ../../CHANGELOG.md >> "$newtab"
EOF
"$(yarn bin)/marked" ../../CHANGELOG.md
echo """
</div>
""" >> "$newtab"
sed "1,/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/ d" "$newtabtemp" >> "$newtab"
"""
sed "1,/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/ d" "$newtabtemp"
) > "$newtab"
rm "$newtabtemp"

View file

@ -2,7 +2,7 @@
# Run prettier on each staged file that needs it without touching the working tree copy if they differ.
source ./scripts/common
source ./scripts/common.sh
set -Eeuo pipefail
@ -24,37 +24,51 @@ unlock() {
rm "$1"
}
trap "unlock $(git rev-parse --show-toplevel)/.git/index.lock || true" ERR
trap 'unlock $(git rev-parse --show-toplevel)/.git/index.lock || true' ERR
main() {
local stagedFiles=$(cachedFiles)
local stagedFiles
stagedFiles="$(cachedTSLintFiles)"$'\n'"$(cachedPrettierFiles)"
if [ -n "$stagedFiles" ]; then
# Could use git-update-index --cacheinfo to add a file without creating directories and stuff.
local tmpdir=$(mktemp -p . -d "pretty.XXXXXXXXX")
IFS=$'\n'
for file in $stagedFiles; do
if cmp -s <(staged "$file") "$file"; then
echo "WARN: Staged copy of '$file' matches working copy. Modifying both"
lock .git/index.lock
prettier --write "$file"
case "$file" in
*.md | *.css) prettier --write "$file";;
*) tslint --project . --fix "$file";;
esac
unlock .git/index.lock
git add "$file"
else
echo "WARN: Staged copy of '$file' does not match working copy: only prettifying staged copy."
(
local tmpdir
tmpdir=$(mktemp -d "pretty.XXXXXXXXX")
cd "$tmpdir"
mkdir -p $(dirname $file)
mkdir -p "$(dirname "$file")"
lock ../.git/index.lock
staged "$file" | prettier --stdin-filepath "$file" > "$file"
tmpfile=$(mktemp "pretty.XXXXXXXXX")
case "$file" in
*.md | *.css)
staged "$file" | prettier --stdin-filepath "$file" > "$tmpfile" &&
mv "$tmpfile" "$file";;
*)
staged "$file" > "$tmpfile"
tslint -c ../tslint.json --fix "$tmpfile" 2>/dev/null &&
mv "$tmpfile" "$file";;
esac
chmod --reference="../$file" "$file" # match permissions
# Can't hold lock while git add occurs. Hopefully release and reacquire happen fast enough to prevent race.
unlock ../.git/index.lock
GIT_WORK_TREE=. git add "$file"
rm -rf "$tmpdir"
)
fi
done
rm -rf "$tmpdir"
fi
}

View file

@ -4,25 +4,62 @@ set -e
sign_and_submit() {
# Don't trust the return value of web-ext sign.
(source AMOKEYS && (web-ext sign -s build --api-key $AMOKEY --api-secret $AMOSECRET || true))
(source AMOKEYS && (web-ext sign -s build --api-key $AMOKEY --api-secret $AMOSECRET "$@" || true))
}
publish_beta_nonewtab() {
yarn run clean
yarn run build --no-native
scripts/version.js beta
sed 's/tridactyl.vim.betas@cmcaine/tridactyl.vim.betas.nonewtab@cmcaine/' -i build/manifest.json
sed '/\s*"newtab":.*/d' -i build/manifest.json
sed 's/"name": "Tridactyl"/"name": "Tridactyl: No New Tab"/' -i build/manifest.json
sign_and_submit -a web-ext-artifacts/nonewtab
}
publish_beta() {
npm run clean
npm run build
yarn run clean
yarn run build --no-native
scripts/version.js beta
sed 's/tridactyl.vim@cmcaine/tridactyl.vim.betas@cmcaine/' -i build/manifest.json
sed 's/"name": "Tridactyl"/"name": "Tridactyl: Beta"/' -i build/manifest.json
sign_and_submit
}
build_no_sign_beta(){
yarn run clean
yarn run build --no-native
scripts/version.js beta
sed 's/"name": "Tridactyl"/"name": "Tridactyl: Beta"/' -i build/manifest.json
mkdir -p web-ext-artifacts
$(yarn bin)/web-ext build --source-dir ./build --overwrite-dest
for f in web-ext-artifacts/*.zip; do
mv $f ${f%.zip}.xpi
done
}
build_no_sign_stable(){
yarn run clean
yarn run build --no-native
sed 's/tridactyl.vim.betas@cmcaine/tridactyl.vim@cmcaine/' -i build/manifest.json
mkdir -p web-ext-artifacts
$(yarn bin)/web-ext build --source-dir ./build --overwrite-dest
for f in web-ext-artifacts/*.zip; do
mv $f ${f%.zip}.xpi
done
}
publish_stable() {
npm run clean
npm run build
yarn run clean
yarn run build --no-native
sed 's/tridactyl.vim.betas@cmcaine/tridactyl.vim@cmcaine/' -i build/manifest.json
sign_and_submit
tar --exclude-from=.gitignore -czf ../public_html/betas/tridactyl_source.tar.gz .
tar --exclude-from=.gitignore -czf ../../public_html/betas/tridactyl_source.tar.gz .
}
case $1 in
stable) publish_stable;;
nosignstable) build_no_sign_stable;;
nosignbeta) build_no_sign_beta;;
nonewtab) publish_beta_nonewtab;;
*|beta) publish_beta;;
esac

12
scripts/thanks Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
export LAST_VERSION="$1"
allcontributors="$(git shortlog -sn $LAST_VERSION..HEAD | cut -c8- | awk '!seen[$0]++' | paste -sd "," - | sed 's/,/, /g')"
newcontributors="$(diff --changed-group-format='%<' --unchanged-group-format='' <(git shortlog -sn $LAST_VERSION..HEAD | cut -c8- | awk '!seen[$0]++'| sort) <(git shortlog -sn $LAST_VERSION | cut -c8- | awk '!seen[$0]++'| sort) | paste -sd "," - | sed 's/,/, /g')"
echo "Thanks to all of our contributors for this release: $allcontributors"
echo
echo "Extra special thanks go to $newcontributors"
echo
echo Last, but not least - thank you to everyone who reported issues.

View file

@ -1,64 +1,94 @@
#!/usr/bin/env node
const {exec} = require('child_process')
const { exec } = require("child_process")
function bump_version(versionstr, component = 2) {
const versionarr = versionstr.split('.')
const versionarr = versionstr.split(".")
versionarr[component] = Number(versionarr[component]) + 1
for (let smaller = component + 1; smaller <= 2; smaller++) {
versionarr[smaller] = 0
}
return versionarr.join('.')
return versionarr.join(".")
}
async function add_beta(versionstr) {
return new Promise((resolve, err)=>{
exec('git rev-list --count HEAD', (execerr, stdout, stderr)=>{
return new Promise((resolve, err) => {
exec("git rev-list --count HEAD", (execerr, stdout, stderr) => {
if (execerr) err(execerr)
resolve(versionstr + "pre" + stdout.trim())
})
})
}
function make_update_json(versionstr){
updatejson = {
"addons": {
"tridactyl.vim.betas@cmcaine.co.uk": {
"updates": [{
"version": versionstr,
"update_link": "https://tridactyl.cmcaine.co.uk/betas/tridactyl-" + versionstr + "-an+fx.xpi"
},]
}
}
function make_update_json(versionstr) {
return {
addons: {
"tridactyl.vim.betas@cmcaine.co.uk": {
updates: [
{
version: versionstr,
update_link:
"https://tridactyl.cmcaine.co.uk/betas/tridactyl_beta-" +
versionstr +
"-an+fx.xpi",
},
],
},
"tridactyl.vim.betas.nonewtab@cmcaine.co.uk": {
updates: [
{
version: versionstr,
update_link:
"https://tridactyl.cmcaine.co.uk/betas/nonewtab/tridactyl_no_new_tab_beta-" +
versionstr +
"-an+fx.xpi",
},
],
},
},
}
return updatejson
}
function save_manifest(filename, manifest) {
// Save file
const fs = require('fs')
const fs = require("fs")
fs.writeFileSync(filename, JSON.stringify(manifest, null, 4))
}
async function main() {
let filename, manifest
switch (process.argv[2]) {
case 'bump':
case "bump":
// Load src manifest and bump
filename = './src/manifest.json'
manifest = require('.' + filename)
manifest.version = bump_version(manifest.version, Number(process.argv[3]))
filename = "./src/manifest.json"
manifest = require("." + filename)
manifest.version = bump_version(
manifest.version,
Number(process.argv[3]),
)
save_manifest(filename, manifest)
exec(`git add ${filename} && git commit -m 'release ${manifest.version}' && git tag ${manifest.version}`)
exec(
`git add ${filename} && git commit -m 'release ${
manifest.version
}' && git tag ${manifest.version}`,
)
console.log(
`Make sure you use the release checklist before committing this.`,
)
console.log(`https://github.com/tridactyl/tridactyl/issues/714`)
break
case 'beta':
filename = './build/manifest.json'
manifest = require('.' + filename)
case "beta":
filename = "./build/manifest.json"
manifest = require("." + filename)
manifest.version = await add_beta(manifest.version)
manifest.applications.gecko.update_url = "https://tridactyl.cmcaine.co.uk/betas/updates.json"
manifest.applications.gecko.update_url =
"https://tridactyl.cmcaine.co.uk/betas/updates.json"
// Make and write updates.json
save_manifest("../public_html/betas/updates.json",make_update_json(manifest.version))
save_manifest(
"../../public_html/betas/updates.json",
make_update_json(manifest.version),
)
// Save manifest.json
save_manifest(filename, manifest)

View file

@ -22,60 +22,59 @@ export DISPLAY=
PREREQUISITES="tput printf 7z wine"
MIN_WINE_VER="3"
MIN_WINE_VER="4"
MIN_7ZIP_VER="16"
checkRequiredVersions() {
if [ -z "$(7z \
| awk '/Version/{print $3}' \
| grep "${MIN_7ZIP_VER}")" ]; then
if ! 7z | awk '/Version/{print $3}' | grep -q "${MIN_7ZIP_VER}"; then
colorEcho \
"[-] p7zip minimum version ${MIN_7ZIP_VER} required\n" \
'[-] p7zip minimum version '"${MIN_7ZIP_VER}"' required\n' \
"alert"
exit -1
exit 1
fi
if [ -z "$(wine --version 2> /dev/null \
| grep "wine-${MIN_WINE_VER}")" ]; then
colorEcho \
"[-] wine minimum version ${MIN_WINE_VER} required\n" \
if ! wine --version 2> /dev/null | grep -q "wine-${MIN_WINE_VER}"; then
colorecho \
'[-] wine minimum version '"${MIN_WINE_VER}"' required\n' \
"alert"
exit -1
exit 1
fi
}
stripWhitespace() {
local input="$@"
printf "${input}\n" | tr -d "[:space:]"
local input="$*"
printf '%s\n' "${input}" | tr -d "[:space:]"
}
colorEcho() {
local COLOR_RESET=$(tput sgr0 2>/dev/null)
local COLOR_BOLD=$(tput bold 2>/dev/null)
local COLOR_BAD=$(tput setaf 1 2>/dev/null)
local COLOR_GOOD=$(tput setaf 2 2>/dev/null)
local COLOR_RESET;
COLOR_RESET="$(tput sgr0 2>/dev/null)"
local COLOR_BOLD;
COLOR_BOLD="$(tput bold 2>/dev/null)"
local COLOR_BAD;
COLOR_BAD="$(tput setaf 1 2>/dev/null)"
local COLOR_GOOD;
COLOR_GOOD="$(tput setaf 2 2>/dev/null)"
local str="$1"
local color="${COLOR_GOOD}${COLOR_BOLD}"
if [ ! -z "$2" ] \
if [ -n "$2" ] \
&& [ "$(stripWhitespace "$2")" = "alert" ]; then
color="${COLOR_BAD}${COLOR_BOLD}"
fi
printf "${color}${str}${COLOR_RESET}"
printf '%s' "${color}${str}${COLOR_RESET}"
}
checkPrerequisite() {
local bin_name="$1"
local bin_loc=$(which "${bin_name}" 2>/dev/null)
if [ -z "${bin_loc}" ] \
|| [ ! -f "${bin_loc}" ]; then
printf " - '$1' not found, quitting ...\n"
exit -1
if command -v "${bin_name}" 1>/dev/null 2>/dev/null; then
printf '%s\n' " - '${bin_name}' found."
else
printf " - '${bin_name}' found at ${bin_loc}\n"
printf '%s\n' " - '$1' not found, quitting ..."
exit 1
fi
}
@ -86,7 +85,7 @@ mainFunction() {
## Check prerequisites
colorEcho "[+] Checking prerequisites ...\n"
colorEcho '[+] Checking prerequisites ...\n'
for bin in ${PREREQUISITES}; do
checkPrerequisite "${bin}"
done
@ -102,7 +101,7 @@ mainFunction() {
## Download Python and Pip
colorEcho "[+] Downloading necessary files ...\n"
colorEcho '[+] Downloading necessary files ...\n'
if [ ! -f "${WINPY_EXE}" ]; then
wget \
@ -112,7 +111,7 @@ mainFunction() {
if [ ! "$(sha256sum "${WINPY_EXE}" \
| cut -d" " -f1)" = ${WINPY_HASH} ]; then
colorEcho "[-] ${WINPY_EXE} has incorrect hash, quitting ...\n"
colorEcho '[-] '"${WINPY_EXE}"' has incorrect hash, quitting ...\n'
exit 1
fi
@ -122,17 +121,17 @@ mainFunction() {
local winepython="wine $PYDIR/python.exe"
if [ ! -f "$PYDIR/python.exe" ]; then
colorEcho "[+] Extract Python-${PYVER}\n"
colorEcho '[+] Extract Python-'${PYVER}'\n'
7z x "${DLDIR}/winpython-${PYVER}.exe" "python-$PYVER" -o"$BUILDROOT"
$winepython -m pip install --upgrade pip
colorEcho "[+] Installing PyInstaller ...\n"
colorEcho '[+] Installing PyInstaller ...\n'
$winepython -m pip install pyinstaller
fi
## Compile with PyInstaller
colorEcho "[+] Compiling with PyInstaller under Wine ...\n"
colorEcho '[+] Compiling with PyInstaller under Wine ...\n'
rm -rf "${OUTDIR}"
PYTHONHASHSEED=1 wine "$PYDIR"/Scripts/pyinstaller.exe \
--clean \
@ -146,7 +145,7 @@ mainFunction() {
"$TRIDIR/native/native_main.py"
## Test the compiled EXE
colorEcho "[+] Checking compiled binary ...\n"
colorEcho '[+] Checking compiled binary ...\n'
OUTFILE="${OUTDIR}/native_main.exe"
cp "$OUTFILE" "$TRIDIR"/web-ext-artifacts/
@ -155,13 +154,13 @@ mainFunction() {
"$TRIDIR/native/gen_native_message.py" cmd..version \
| wine "$TRIDIR"/web-ext-artifacts/native_main.exe
printf "\n"
colorEcho "[+] PyInstaller with Wine was successful!\n"
printf '\n'
colorEcho '[+] PyInstaller with Wine was successful!\n'
else
colorEcho \
"[-] PyInstaller compilation failed, quitting ...\n" \
'[-] PyInstaller compilation failed, quitting ...\n' \
"alert"
exit -1
exit 1
fi
}

View file

@ -1,27 +1,34 @@
/** Background script entry point. */
import * as BackgroundController from "./controller_background"
import "./lib/browser_proxy_background"
/* tslint:disable:import-spacing */
import "@src/lib/browser_proxy_background"
import * as controller from "@src/lib/controller"
import * as perf from "@src/perf"
import { listenForCounters } from "@src/perf"
import * as messaging from "@src/lib/messaging"
import * as excmds_background from "@src/.excmds_background.generated"
import { CmdlineCmds } from "@src/background/commandline_cmds"
import { EditorCmds } from "@src/background/editor"
import * as convert from "@src/lib/convert"
import * as config from "@src/lib/config"
import * as dom from "@src/lib/dom"
import * as download_background from "@src/background/download_background"
import * as itertools from "@src/lib/itertools"
import * as keyseq from "@src/lib/keyseq"
import * as request from "@src/lib/requests"
import * as native from "@src/lib/native"
import state from "@src/state"
import * as webext from "@src/lib/webext"
import { AutoContain } from "@src/lib/autocontainers"
import * as extension_info from "@src/lib/extension_info"
import * as omnibox from "@src/background/omnibox"
// Add various useful modules to the window for debugging
import * as messaging from "./messaging"
import * as excmds from "./.excmds_background.generated"
import * as commandline_background from "./commandline_background"
import * as convert from "./convert"
import * as config from "./config"
import * as dom from "./dom"
import * as download_background from "./download_background"
import * as itertools from "./itertools"
import * as keyseq from "./keyseq"
import * as request from "./requests"
import * as native from "./native_background"
import state from "./state"
import * as webext from "./lib/webext"
import { AutoContain } from "./lib/autocontainers"
;(window as any).tri = Object.assign(Object.create(null), {
; (window as any).tri = Object.assign(Object.create(null), {
messaging,
excmds,
commandline_background,
excmds: excmds_background,
convert,
config,
dom,
@ -33,54 +40,57 @@ import { AutoContain } from "./lib/autocontainers"
state,
webext,
l: prom => prom.then(console.log).catch(console.error),
contentLocation: window.location,
perf,
})
// Send commandline to controller
commandline_background.onLine.addListener(BackgroundController.acceptExCmd)
import { HintingCmds } from "@src/background/hinting"
// Set up our controller to execute background-mode excmds. All code
// running from this entry point, which is to say, everything in the
// background script, will use the excmds that we give to the module
// here.
controller.setExCmds({
"": excmds_background,
"ex": CmdlineCmds,
"text": EditorCmds,
"hint": HintingCmds
})
messaging.addListener("excmd_background", messaging.attributeCaller(excmds_background))
messaging.addListener("controller_background", messaging.attributeCaller(controller))
// {{{ Clobber CSP
// This should be removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1267027 is fixed
function addCSPListener() {
browser.webRequest.onHeadersReceived.addListener(
request.clobberCSP,
{ urls: ["<all_urls>"], types: ["main_frame"] },
["blocking", "responseHeaders"],
)
}
function removeCSPListener() {
browser.webRequest.onHeadersReceived.removeListener(request.clobberCSP)
}
config.getAsync("csp").then(csp => csp === "clobber" && addCSPListener())
browser.storage.onChanged.addListener((changes, areaname) => {
if ("userconfig" in changes) {
if (changes.userconfig.newValue.csp === "clobber") {
addCSPListener()
} else {
removeCSPListener()
// {{{ tri.contentLocation
// When loading the background, use the active tab to know what the current content url is
browser.tabs.query({ currentWindow: true, active: true }).then(t => {
(window as any).tri.contentLocation = new URL(t[0].url)
})
// After that, on every tab change, update the current url
let contentLocationCount = 0
browser.tabs.onActivated.addListener(ev => {
const myId = contentLocationCount + 1
contentLocationCount = myId
browser.tabs.get(ev.tabId).then(t => {
// Note: we're using contentLocationCount and myId in order to make sure that only the last onActivated event is used in order to set contentLocation
// This is needed because otherWise the following chain of execution might happen: onActivated1 => onActivated2 => tabs.get2 => tabs.get1
if (contentLocationCount === myId) {
(window as any).tri.contentLocation = new URL(t.url)
}
}
})
})
// }}}
// Prevent Tridactyl from being updated while it is running in the hope of fixing #290
browser.runtime.onUpdateAvailable.addListener(_ => {})
browser.runtime.onUpdateAvailable.addListener(_ => undefined)
browser.runtime.onStartup.addListener(_ => {
config.getAsync("autocmds", "TriStart").then(aucmds => {
let hosts = Object.keys(aucmds)
const hosts = Object.keys(aucmds)
// If there's only one rule and it's "all", no need to check the hostname
if (hosts.length == 1 && hosts[0] == ".*") {
BackgroundController.acceptExCmd(aucmds[hosts[0]])
if (hosts.length === 1 && hosts[0] === ".*") {
controller.acceptExCmd(aucmds[hosts[0]])
} else {
native.run("hostname").then(hostname => {
for (let host of hosts) {
for (const host of hosts) {
if (hostname.content.match(host)) {
BackgroundController.acceptExCmd(aucmds[host])
controller.acceptExCmd(aucmds[host])
}
}
})
@ -88,11 +98,27 @@ browser.runtime.onStartup.addListener(_ => {
})
})
// Nag people about updates.
// Hope that they're on a tab we can access.
config.getAsync("update", "nag").then(nag => {
if (nag === true) excmds_background.updatecheck("auto_polite")
})
// }}}
// {{{ AUTOCOMMANDS
// We could use ev.previousTabId here, but that field is empty when a
// tab is closed, and we do want to run "TabLeft" commands when that
// happens. Instead, we assume that the user can only be in one tab at
// a time and the last tab we entered has to be the one we're leaving.
let curTab = null
browser.tabs.onActivated.addListener(ev => {
let ignore = _ => _
const ignore = _ => _
if (curTab !== null) {
// messaging.messageTab failing can happen when leaving privileged tabs (e.g. about:addons)
// messaging.messageTab failing can happen when leaving
// privileged tabs (e.g. about:addons) or when the tab is
// being closed.
messaging
.messageTab(curTab, "excmd_content", "loadaucmds", ["TabLeft"])
.catch(ignore)
@ -103,28 +129,24 @@ browser.tabs.onActivated.addListener(ev => {
.catch(ignore)
})
// }}}
// {{{ AUTOCONTAINERS
let aucon = new AutoContain()
extension_info.init()
const aucon = new AutoContain()
// Handle cancelled requests as a result of autocontain.
browser.webRequest.onCompleted.addListener(
details => {
if (aucon.getCancelledRequest(details.tabId)) {
aucon.clearCancelledRequests(details.tabId)
}
},
{ urls: ["<all_urls"], types: ["main_frame"] },
)
browser.webRequest.onCompleted.addListener(aucon.completedRequestListener, {
urls: ["<all_urls>"],
types: ["main_frame"],
})
browser.webRequest.onErrorOccurred.addListener(
details => {
if (aucon.getCancelledRequest(details.tabId)) {
aucon.clearCancelledRequests(details.tabId)
}
},
{ urls: ["<all_urls>"], types: ["main_frame"] },
)
browser.webRequest.onErrorOccurred.addListener(aucon.completedRequestListener, {
urls: ["<all_urls>"],
types: ["main_frame"],
})
// Contain autocmd.
browser.webRequest.onBeforeRequest.addListener(
@ -132,4 +154,45 @@ browser.webRequest.onBeforeRequest.addListener(
{ urls: ["<all_urls>"], types: ["main_frame"] },
["blocking"],
)
browser.tabs.onCreated.addListener(
aucon.tabCreatedListener,
)
// }}}
// {{{ PERFORMANCE LOGGING
// An object to collect all of our statistics in one place.
const statsLogger: perf.StatsLogger = new perf.StatsLogger()
messaging.addListener(
"performance_background",
messaging.attributeCaller(statsLogger),
)
// Listen for statistics from the background script and store
// them. Set this one up to log directly to the statsLogger instead of
// going through messaging.
const perfObserver = listenForCounters(statsLogger)
window.tri = Object.assign(window.tri || Object.create(null), {
// Attach the perf observer to the window object, since there
// appears to be a bug causing performance observers to be GC'd
// even if they're still the target of a callback.
perfObserver,
// Also attach the statsLogger so we can access our stats from the
// console.
statsLogger,
})
// }}}
// {{{ OMNIBOX
omnibox.init()
// }}}
// {{{ Obey Mozilla's orders https://github.com/tridactyl/tridactyl/issues/1800
native.unfixamo();
/// }}}

View file

@ -0,0 +1,15 @@
import { getCommandlineFns } from "@src/lib/commandline_cmds"
import { messageActiveTab } from "@src/lib/messaging"
const functions = getCommandlineFns({} as any)
type ft = typeof functions
type ArgumentsType<T> = T extends (...args: infer U) => any ? U: never;
export const CmdlineCmds = new Proxy (functions as any, {
get(target, property) {
if (target[property]) {
return (...args) => messageActiveTab("commandline_cmd", property as string, args)
}
return target[property]
}
}) as { [k in keyof ft]: (...args: ArgumentsType<ft[k]>) => Promise<ReturnType<ft[k]>> }

View file

@ -0,0 +1,56 @@
import * as controller from "@src/lib/controller"
import * as Native from "@src/lib/native"
export async function source(filename = "auto") {
let rctext = ""
if (filename === "auto") {
rctext = await Native.getrc()
} else {
rctext = (await Native.read(filename)).content
}
if (rctext === undefined) return false
await runRc(rctext)
return true
}
async function fetchConfig(url: string) {
const response = await fetch(url)
const reader = response.body.getReader()
let rctext = ""
const decoder = new TextDecoder("utf-8")
while (true) {
const { value: chunk, done: isDone } = await reader.read()
if (isDone) return rctext
rctext += decoder.decode(chunk)
}
}
export async function sourceFromUrl(url: string) {
const rctext = await fetchConfig(url)
if (!rctext) return false
await runRc(rctext)
return true
}
export async function writeRc(conf: string, force = false, filename = "auto") {
let path: string
if (filename === "auto") {
path = await Native.getrcpath()
} else {
path = filename
}
return await Native.writerc(path, force, conf)
}
export async function runRc(rc: string) {
for (const cmd of rcFileToExCmds(rc)) {
await controller.acceptExCmd(cmd)
}
}
export function rcFileToExCmds(rcText: string): string[] {
const excmds = rcText.split("\n")
// Remove empty and comment lines
return excmds.filter(x => /\S/.test(x) && !x.trim().startsWith('"'))
}

View file

@ -0,0 +1,159 @@
/**
* Background download-related functions
*/
import * as Native from "@src/lib/native"
import { getDownloadFilenameForUrl } from "@src/lib/url_util"
/** Construct an object URL string from a given data URL
*
* This is needed because feeding a data URL directly to downloads.download()
* causes "Error: Access denied for URL"
*
* @param dataUrl the URL to make an object URL from
* @return object URL that can be fed to the downloads API
*
*/
function objectUrlFromDataUrl(dataUrl: URL): string {
const b64 = dataUrl.pathname.split(",", 2)[1]
const binaryF = atob(b64)
const dataArray = new Uint8Array(binaryF.length)
for (let i = 0, len = binaryF.length; i < len; ++i) {
dataArray[i] = binaryF.charCodeAt(i)
}
return URL.createObjectURL(new Blob([dataArray]))
}
/** Download a given URL to disk
*
* Normal URLs are downloaded normally. Data URLs are handled more carefully
* as it's not allowed in WebExt land to just call downloads.download() on
* them
*
* @param url the URL to download
* @param saveAs prompt user for a filename
*/
export async function downloadUrl(url: string, saveAs: boolean) {
const urlToSave = new URL(url)
let urlToDownload
if (urlToSave.protocol === "data:") {
urlToDownload = objectUrlFromDataUrl(urlToSave)
} else {
urlToDownload = urlToSave.href
}
const fileName = getDownloadFilenameForUrl(urlToSave)
// Save location limitations:
// - download() can't save outside the downloads dir without popping
// the Save As dialog
// - Even if the dialog is popped, it doesn't seem to be possible to
// feed in the dirctory for next time, and FF doesn't remember it
// itself (like it does if you right-click-save something)
const downloadPromise = browser.downloads.download({
url: urlToDownload,
filename: fileName,
saveAs,
})
// TODO: at this point, could give feeback using the promise returned
// by downloads.download(), needs status bar to show it (#90)
// By awaiting the promise, we ensure that if it errors, the error will be
// thrown by this function too.
await downloadPromise
}
/** Dowload a given URL to disk
*
* This behaves mostly like downloadUrl, except that this function will use the native messenger in order to move the file to `saveAs`.
*
* Note: this requires a native messenger >=0.1.9. Make sure to nativegate for this.
*
* @param url the URL to download
* @param saveAs If beginning with a slash, this is the absolute path the document should be moved to. If the first character of the string is a tilda, it will be expanded to an absolute path to the user's home directory. If saveAs begins with any other character, it will be considered a path relative to where the native messenger binary is located (e.g. "$HOME/.local/share/tridactyl" on linux).
* If saveAs points to a directory, the name of the document will be inferred from the URL and the document will be placed inside the directory. If saveAs points to an already existing file, the document will be saved in the downloads directory but wont be moved to where it should be ; an error will be thrown. If any of the directories referred to in saveAs do not exist, the file will be kept in the downloads directory but won't be moved to where it should be.
*/
export async function downloadUrlAs(url: string, saveAs: string) {
if (!(await Native.nativegate("0.1.9", true))) return
const urlToSave = new URL(url)
let urlToDownload
if (urlToSave.protocol === "data:") {
urlToDownload = objectUrlFromDataUrl(urlToSave)
} else {
urlToDownload = urlToSave.href
}
const fileName = getDownloadFilenameForUrl(urlToSave)
const downloadId = await browser.downloads.download({
conflictAction: "uniquify",
url: urlToDownload,
filename: fileName,
})
// We want to return a promise that will resolve once the file has been moved somewhere else
return new Promise((resolve, reject) => {
const onDownloadComplete = async downloadDelta => {
if (downloadDelta.id !== downloadId) {
return
}
// Note: this might be a little too drastic. For example, files that encounter a problem while being downloaded and the download of which is restarted by a user won't be moved
// This seems acceptable for now as taking all states into account seems quite difficult
if (
downloadDelta.state &&
downloadDelta.state.current !== "in_progress"
) {
browser.downloads.onChanged.removeListener(onDownloadComplete)
const downloadItem = (await browser.downloads.search({
id: downloadId,
}))[0]
if (downloadDelta.state.current === "complete") {
const operation = await Native.move(
downloadItem.filename,
saveAs,
)
if (operation.code !== 0) {
reject(
new Error(
`'${
downloadItem.filename
}' could not be moved to '${saveAs}'. Make sure it doesn't already exist and that all directories of the path exist.`,
),
)
} else {
resolve(operation)
}
} else {
reject(
new Error(
`'${
downloadItem.filename
}' state not in_progress anymore but not complete either (would have been moved to '${saveAs}')`,
),
)
}
}
}
browser.downloads.onChanged.addListener(onDownloadComplete)
})
}
import * as Messaging from "@src/lib/messaging"
// Get messages from content
Messaging.addListener(
"download_background",
Messaging.attributeCaller({
downloadUrl,
downloadUrlAs,
}),
)

14
src/background/editor.ts Normal file
View file

@ -0,0 +1,14 @@
import { messageActiveTab } from "@src/lib/messaging.ts"
import * as _EditorCmds from "@src/lib/editor.ts"
type cmdsType = typeof _EditorCmds
type ArgumentsType<T> = T extends (elem, ...args: infer U) => any ? U: never;
export const EditorCmds = new Proxy(_EditorCmds as any, {
get(target, property) {
if (target[property]) {
return (...args) => messageActiveTab("editorfn_content", property as string, args)
}
return target[property]
}
}) as { [k in keyof cmdsType]: (...args: ArgumentsType<cmdsType[k]>) => Promise<ReturnType<cmdsType[k]>> }

15
src/background/hinting.ts Normal file
View file

@ -0,0 +1,15 @@
import { messageActiveTab } from "@src/lib/messaging"
import * as hinting_content from "@src/content/hinting"
const functions = hinting_content.getHintCommands()
type ft = typeof functions
type ArgumentsType<T> = T extends (...args: infer U) => any ? U : never;
export const HintingCmds = new Proxy(functions as any, {
get(target, property) {
if (target[property]) {
return (...args) => messageActiveTab("controller_content", "acceptExCmd", [property].concat(args))
}
return target[property]
}
}) as { [k in keyof ft]: (...args: ArgumentsType<ft[k]>) => Promise<ReturnType<ft[k]>> }

33
src/background/omnibox.ts Normal file
View file

@ -0,0 +1,33 @@
/**
* Allows users to enter tridactyl commands from the omnibox by using
* the `:` keyword.
*/
import * as controller from "@src/lib/controller"
export async function inputStartedListener() {
}
export async function inputChangedListener(
currentInput: string,
emitSuggestion: (suggestions: browser.omnibox.SuggestResult[]) => void
) {
}
export async function inputEnteredListener(
input: string, disposition:
browser.omnibox.OnInputEnteredDisposition) {
controller.acceptExCmd(input)
}
export async function inputCancelledListener() {
}
export async function init() {
browser.omnibox.onInputStarted.addListener(inputStartedListener)
browser.omnibox.onInputChanged.addListener(inputChangedListener)
browser.omnibox.onInputEntered.addListener(inputEnteredListener)
browser.omnibox.onInputCancelled.addListener(inputCancelledListener)
browser.omnibox.setDefaultSuggestion({
description: `Execute a Tridactyl exstr (for example, "tabopen -c container www.google.com")`,
})
}

View file

@ -1,69 +0,0 @@
import { activeTabId } from "./lib/webext"
import * as Messaging from "./messaging"
export type onLineCallback = (exStr: string) => void
/** CommandLine API for inclusion in background script
Receives messages from commandline_frame
*/
export const onLine = {
addListener: function(cb: onLineCallback) {
listeners.add(cb)
return () => {
listeners.delete(cb)
}
},
}
const listeners = new Set<onLineCallback>()
/** Receive events from commandline_frame and pass to listeners */
function recvExStr(exstr: string) {
for (let listener of listeners) {
listener(exstr)
}
}
/** Helpers for completions */
async function currentWindowTabs(): Promise<browser.tabs.Tab[]> {
return await browser.tabs.query({ currentWindow: true })
}
async function history(): Promise<browser.history.HistoryItem[]> {
return await browser.history.search({
text: "",
maxResults: 50,
startTime: 0,
})
}
async function allWindowTabs(): Promise<browser.tabs.Tab[]> {
return browser.tabs.query({})
}
export async function show(focus = true) {
Messaging.messageActiveTab("commandline_content", "show")
if (focus) {
Messaging.messageActiveTab("commandline_content", "focus")
Messaging.messageActiveTab("commandline_frame", "focus")
}
}
export async function hide(tabid?) {
if (!tabid) tabid = await activeTabId()
Messaging.messageTab(tabid, "commandline_content", "hide")
Messaging.messageTab(tabid, "commandline_content", "blur")
}
Messaging.addListener(
"commandline_background",
Messaging.attributeCaller({
allWindowTabs,
currentWindowTabs,
history,
recvExStr,
show,
hide,
}),
)

View file

@ -1,327 +1,305 @@
/** # Command line functions
*
* This file contains functions to interact with the command line.
*
* If you want to bind them to keyboard shortcuts, be sure to prefix them with "ex.". For example, if you want to bind control-p to `prev_completion`, use:
*
* ```
* bind --mode=ex <C-p> ex.prev_completion
* ```
*
* Note that you can also bind Tridactyl's [editor functions](/static/docs/modules/_src_lib_editor_.html) in the command line.
*
* Contrary to the main tridactyl help page, this one doesn't tell you whether a specific function is bound to something. For now, you'll have to make do with `:bind` and `:viewconfig`.
*
*/
/** ignore this line */
/** Script used in the commandline iframe. Communicates with background. */
import "./lib/html-tagged-template"
import * as perf from "@src/perf"
import "@src/lib/number.clamp"
import "@src/lib/html-tagged-template"
import { TabAllCompletionSource } from "@src/completions/TabAll"
import { BufferCompletionSource } from "@src/completions/Tab"
import { BmarkCompletionSource } from "@src/completions/Bmark"
import { ExcmdCompletionSource } from "@src/completions/Excmd"
import { FileSystemCompletionSource } from "@src/completions/FileSystem"
import { GuisetCompletionSource } from "@src/completions/Guiset"
import { HelpCompletionSource } from "@src/completions/Help"
import { HistoryCompletionSource } from "@src/completions/History"
import { PreferenceCompletionSource } from "@src/completions/Preferences"
import { RssCompletionSource } from "@src/completions/Rss"
import { SessionsCompletionSource } from "@src/completions/Sessions"
import { SettingsCompletionSource } from "@src/completions/Settings"
import { WindowCompletionSource } from "@src/completions/Window"
import { ExtensionsCompletionSource } from "@src/completions/Extensions"
import * as Messaging from "@src/lib/messaging"
import "@src/lib/number.clamp"
import state from "@src/state"
import Logger from "@src/lib/logging"
import { theme } from "@src/content/styling"
import * as Completions from "./completions"
import { BufferAllCompletionSource } from "./completions/BufferAll"
import { BufferCompletionSource } from "./completions/Buffer"
import { BmarkCompletionSource } from "./completions/Bmark"
import { ExcmdCompletionSource } from "./completions/Excmd"
import { HistoryCompletionSource } from "./completions/History"
import { SettingsCompletionSource } from "./completions/Settings"
import * as Messaging from "./messaging"
import * as Config from "./config"
import * as SELF from "./commandline_frame"
import "./number.clamp"
import state from "./state"
import Logger from "./logging"
import { theme } from "./styling"
import * as genericParser from "@src/parsers/genericmode"
import * as tri_editor from "@src/lib/editor"
/** @hidden **/
const logger = new Logger("cmdline")
let activeCompletions: Completions.CompletionSource[] = undefined
let completionsDiv = window.document.getElementById(
"completions",
) as HTMLElement
let clInput = window.document.getElementById(
"tridactyl-input",
) as HTMLInputElement
/** @hidden **/
const commandline_state = {
activeCompletions: undefined,
clInput: (window.document.getElementById("tridactyl-input") as HTMLInputElement),
clear,
cmdline_history_position: 0,
completionsDiv: window.document.getElementById("completions"),
fns: undefined,
getCompletion,
history,
/** @hidden
* This is to handle Escape key which, while the cmdline is focused,
* ends up firing both keydown and input listeners. In the worst case
* hides the cmdline, shows and refocuses it and replaces its text
* which could be the prefix to generate a completion.
* tl;dr TODO: delete this and better resolve race condition
*/
isVisible: false,
keyEvents: new Array<KeyEventLike>(),
refresh_completions,
state,
}
// first theming of commandline iframe
theme(document.querySelector(":root"))
/* This is to handle Escape key which, while the cmdline is focused,
* ends up firing both keydown and input listeners. In the worst case
* hides the cmdline, shows and refocuses it and replaces its text
* which could be the prefix to generate a completion.
* tl;dr TODO: delete this and better resolve race condition
*/
let isVisible = false
/** @hidden **/
function resizeArea() {
if (isVisible) {
Messaging.message("commandline_background", "show")
if (commandline_state.isVisible) {
Messaging.messageOwnTab("commandline_content", "show")
Messaging.messageOwnTab("commandline_content", "focus")
focus()
}
}
// This is a bit loosely defined at the moment.
// Should work so long as there's only one completion source per prefix.
/** @hidden
* This is a bit loosely defined at the moment.
* Should work so long as there's only one completion source per prefix.
*/
function getCompletion() {
for (const comp of activeCompletions) {
if (!commandline_state.activeCompletions) return undefined
for (const comp of commandline_state.activeCompletions) {
if (comp.state === "normal" && comp.completion !== undefined) {
return comp.completion
}
}
}
commandline_state.getCompletion = getCompletion
function enableCompletions() {
if (!activeCompletions) {
activeCompletions = [
new BmarkCompletionSource(completionsDiv),
new BufferAllCompletionSource(completionsDiv),
new BufferCompletionSource(completionsDiv),
new ExcmdCompletionSource(completionsDiv),
new SettingsCompletionSource(completionsDiv),
new HistoryCompletionSource(completionsDiv),
/** @hidden **/
export function enableCompletions() {
if (!commandline_state.activeCompletions) {
commandline_state.activeCompletions = [
// FindCompletionSource,
BmarkCompletionSource,
TabAllCompletionSource,
BufferCompletionSource,
ExcmdCompletionSource,
FileSystemCompletionSource,
GuisetCompletionSource,
HelpCompletionSource,
HistoryCompletionSource,
PreferenceCompletionSource,
RssCompletionSource,
SessionsCompletionSource,
SettingsCompletionSource,
WindowCompletionSource,
ExtensionsCompletionSource,
]
.map(constructorr => {
try {
return new constructorr(commandline_state.completionsDiv)
} catch (e) {}
})
.filter(c => c)
const fragment = document.createDocumentFragment()
activeCompletions.forEach(comp => fragment.appendChild(comp.node))
completionsDiv.appendChild(fragment)
commandline_state.activeCompletions.forEach(comp => fragment.appendChild(comp.node))
commandline_state.completionsDiv.appendChild(fragment)
logger.debug(commandline_state.activeCompletions)
}
}
/* document.addEventListener("DOMContentLoaded", enableCompletions) */
let noblur = e => setTimeout(() => clInput.focus(), 0)
/** @hidden **/
const noblur = e => setTimeout(() => commandline_state.clInput.focus(), 0)
/** @hidden **/
export function focus() {
clInput.focus()
clInput.addEventListener("blur", noblur)
}
async function sendExstr(exstr) {
Messaging.message("commandline_background", "recvExStr", [exstr])
commandline_state.clInput.focus()
commandline_state.clInput.removeEventListener("blur", noblur)
commandline_state.clInput.addEventListener("blur", noblur)
}
/** @hidden **/
let HISTORY_SEARCH_STRING: string
/* Command line keybindings */
clInput.addEventListener("keydown", function(keyevent) {
switch (keyevent.key) {
case "Enter":
process()
break
case "j":
if (keyevent.ctrlKey) {
// stop Firefox from giving focus to the omnibar
keyevent.preventDefault()
keyevent.stopPropagation()
process()
}
break
case "m":
if (keyevent.ctrlKey) {
process()
}
break
case "Escape":
/** @hidden
* Command line keybindings
**/
const keyParser = keys => genericParser.parser("exmaps", keys)
/** @hidden **/
let history_called = false
/** @hidden **/
let prev_cmd_called_history = false
/** @hidden **/
commandline_state.clInput.addEventListener(
"keydown",
function(keyevent: KeyboardEvent) {
if (!keyevent.isTrusted) return
commandline_state.keyEvents.push(keyevent)
const response = keyParser(commandline_state.keyEvents)
if (response.isMatch) {
keyevent.preventDefault()
hide_and_clear()
break
keyevent.stopImmediatePropagation()
} else {
// Ideally, all keys that aren't explicitly bound to an ex command
// should be bound to a "self-insert" command that would input the
// key itself. Because it's not possible to generate events as if
// they originated from the user, we can't do this, but we still
// need to simulate it, in order to have history() work.
prev_cmd_called_history = false
}
if (response.value) {
commandline_state.keyEvents = []
history_called = false
// Todo: fish-style history search
// persistent history
case "ArrowUp":
history(-1)
break
case "ArrowDown":
history(1)
break
case "a":
if (keyevent.ctrlKey) {
keyevent.preventDefault()
keyevent.stopPropagation()
setCursor()
}
break
case "e":
if (keyevent.ctrlKey) {
keyevent.preventDefault()
keyevent.stopPropagation()
setCursor(clInput.value.length)
}
break
case "u":
if (keyevent.ctrlKey) {
keyevent.preventDefault()
keyevent.stopPropagation()
clInput.value = clInput.value.slice(
clInput.selectionStart,
clInput.value.length,
)
setCursor()
}
break
case "k":
if (keyevent.ctrlKey) {
keyevent.preventDefault()
keyevent.stopPropagation()
clInput.value = clInput.value.slice(0, clInput.selectionStart)
}
break
// Clear input on ^C if there is no selection
// Todo: hard mode: vi style editing on cli, like set -o mode vi
// should probably just defer to another library
case "c":
if (
keyevent.ctrlKey &&
!clInput.value.substring(
clInput.selectionStart,
clInput.selectionEnd,
)
) {
hide_and_clear()
}
break
case "f":
if (keyevent.ctrlKey) {
// Stop ctrl+f from doing find
keyevent.preventDefault()
keyevent.stopPropagation()
tabcomplete()
}
break
case "Tab":
// Stop tab from losing focus
keyevent.preventDefault()
keyevent.stopPropagation()
if (keyevent.shiftKey) {
activeCompletions.forEach(comp => comp.prev())
// If excmds start with 'ex.' they're coming back to us anyway, so skip that.
// This is definitely a hack. Should expand aliases with exmode, etc.
// but this whole thing should be scrapped soon, so whatever.
if (response.value.startsWith("ex.")) {
const funcname = response.value.slice(3)
commandline_state.fns[funcname]()
prev_cmd_called_history = history_called
} else {
activeCompletions.forEach(comp => comp.next())
// Send excmds directly to our own tab, which fixes the
// old bug where a command would be issued in one tab but
// land in another because the active tab had
// changed. Background-mode excmds will be received by the
// own tab's content script and then bounced through a
// shim to the background, but the latency increase should
// be acceptable becuase the background-mode excmds tend
// to be a touch less latency-sensitive.
Messaging.messageOwnTab("controller_content", "acceptExCmd", [
response.value,
]).then(_ => (prev_cmd_called_history = history_called))
}
// tabcomplete()
break
} else {
commandline_state.keyEvents = response.keys
}
},
true,
)
case " ":
const command = getCompletion()
activeCompletions.forEach(comp => (comp.completion = undefined))
if (command) fillcmdline(command, false)
break
}
export function refresh_completions(exstr) {
if (!commandline_state.activeCompletions) enableCompletions()
return Promise.all(
commandline_state.activeCompletions.map(comp =>
comp.filter(exstr).then(() => {
if (comp.shouldRefresh()) {
return resizeArea()
}
}),
),
).catch(err => {
console.error(err)
return []
}) // We can't use the regular logging mechanism because the user is using the command line.
}
// If a key other than the arrow keys was pressed, clear the history search string
if (!(keyevent.key == "ArrowUp" || keyevent.key == "ArrowDown")) {
HISTORY_SEARCH_STRING = undefined
}
/** @hidden **/
let onInputPromise: Promise<any> = Promise.resolve()
/** @hidden **/
commandline_state.clInput.addEventListener("input", () => {
const exstr = commandline_state.clInput.value
// Schedule completion computation. We do not start computing immediately because this would incur a slow down on quickly repeated input events (e.g. maintaining <Backspace> pressed)
setTimeout(async () => {
// Make sure the previous computation has ended
await onInputPromise
// If we're not the current completion computation anymore, stop
if (exstr !== commandline_state.clInput.value) return
onInputPromise = refresh_completions(exstr)
}, 100)
})
clInput.addEventListener("input", () => {
const exstr = clInput.value
// Fire each completion and add a callback to resize area
enableCompletions()
logger.debug(activeCompletions)
activeCompletions.forEach(comp =>
comp.filter(exstr).then(() => resizeArea()),
)
})
let cmdline_history_position = 0
/** @hidden **/
let cmdline_history_current = ""
/** Clears the command line.
* If you intend to close the command line after this, set evlistener to true in order to enable losing focus.
/** @hidden
* Clears the command line.
* If you intend to close the command line after this, set evlistener to true in order to enable losing focus.
* Otherwise, no need to pass an argument.
*/
export function clear(evlistener = false) {
if (evlistener) clInput.removeEventListener("blur", noblur)
clInput.value = ""
cmdline_history_position = 0
if (evlistener) commandline_state.clInput.removeEventListener("blur", noblur)
commandline_state.clInput.value = ""
commandline_state.cmdline_history_position = 0
cmdline_history_current = ""
}
commandline_state.clear = clear
export async function hide_and_clear() {
clear(true)
// Try to make the close cmdline animation as smooth as possible.
Messaging.message("commandline_background", "hide")
// Delete all completion sources - I don't think this is required, but this
// way if there is a transient bug in completions it shouldn't persist.
if (activeCompletions)
activeCompletions.forEach(comp => completionsDiv.removeChild(comp.node))
activeCompletions = undefined
isVisible = false
}
function setCursor(n = 0) {
clInput.setSelectionRange(n, n, "none")
}
function tabcomplete() {
let fragment = clInput.value
let matches = state.cmdHistory.filter(key => key.startsWith(fragment))
let mostrecent = matches[matches.length - 1]
if (mostrecent != undefined) clInput.value = mostrecent
}
/** @hidden **/
function history(n) {
HISTORY_SEARCH_STRING =
HISTORY_SEARCH_STRING === undefined
? clInput.value
: HISTORY_SEARCH_STRING
let matches = state.cmdHistory.filter(key =>
history_called = true
if (!prev_cmd_called_history) {
HISTORY_SEARCH_STRING = commandline_state.clInput.value
}
const matches = state.cmdHistory.filter(key =>
key.startsWith(HISTORY_SEARCH_STRING),
)
if (cmdline_history_position == 0) {
cmdline_history_current = clInput.value
if (commandline_state.cmdline_history_position === 0) {
cmdline_history_current = commandline_state.clInput.value
}
let clamped_ind = matches.length + n - cmdline_history_position
let clamped_ind = matches.length + n - commandline_state.cmdline_history_position
clamped_ind = clamped_ind.clamp(0, matches.length)
const pot_history = matches[clamped_ind]
clInput.value =
pot_history == undefined ? cmdline_history_current : pot_history
commandline_state.clInput.value =
pot_history === undefined ? cmdline_history_current : pot_history
// if there was no clampage, update history position
// there's a more sensible way of doing this but that would require more programmer time
if (clamped_ind == matches.length + n - cmdline_history_position)
cmdline_history_position = cmdline_history_position - n
}
/* Send the commandline to the background script and await response. */
function process() {
const command = getCompletion() || clInput.value
hide_and_clear()
const [func, ...args] = command.trim().split(/\s+/)
if (func.length === 0 || func.startsWith("#")) {
return
}
// Save non-secret commandlines to the history.
if (
!browser.extension.inIncognitoContext &&
!(func === "winopen" && args[0] === "-private")
) {
state.cmdHistory = state.cmdHistory.concat([command])
}
cmdline_history_position = 0
sendExstr(command)
if (clamped_ind === matches.length + n - commandline_state.cmdline_history_position)
commandline_state.cmdline_history_position = commandline_state.cmdline_history_position - n
}
commandline_state.history = history
/** @hidden **/
export function fillcmdline(
newcommand?: string,
trailspace = true,
ffocus = true,
) {
if (trailspace) clInput.value = newcommand + " "
else clInput.value = newcommand
isVisible = true
if (trailspace) commandline_state.clInput.value = newcommand + " "
else commandline_state.clInput.value = newcommand
commandline_state.isVisible = true
let result = Promise.resolve([])
// Focus is lost for some reason.
if (ffocus) {
focus()
clInput.dispatchEvent(new Event("input")) // dirty hack for completions
result = refresh_completions(commandline_state.clInput.value)
}
return result
}
/** Create a temporary textarea and give it to fn. Remove the textarea afterwards
Useful for document.execCommand
*/
/** @hidden
* Create a temporary textarea and give it to fn. Remove the textarea afterwards
*
* Useful for document.execCommand
**/
function applyWithTmpTextArea(fn) {
let textarea
try {
@ -338,7 +316,9 @@ function applyWithTmpTextArea(fn) {
}
}
/** @hidden **/
export async function setClipboard(content: string) {
await Messaging.messageOwnTab("commandline_content", "focus")
applyWithTmpTextArea(scratchpad => {
scratchpad.value = content
scratchpad.select()
@ -348,22 +328,56 @@ export async function setClipboard(content: string) {
} else throw "Failed to copy!"
})
// Return focus to the document
await Messaging.message("commandline_background", "hide")
await Messaging.messageOwnTab("commandline_content", "hide")
return Messaging.messageOwnTab("commandline_content", "blur")
}
export function getClipboard() {
/** @hidden **/
export async function getClipboard() {
await Messaging.messageOwnTab("commandline_content", "focus")
const result = applyWithTmpTextArea(scratchpad => {
scratchpad.focus()
document.execCommand("Paste")
return scratchpad.textContent
})
// Return focus to the document
Messaging.message("commandline_background", "hide")
await Messaging.messageOwnTab("commandline_content", "hide")
await Messaging.messageOwnTab("commandline_content", "blur")
return result
}
/** @hidden **/
export function getContent() {
return clInput.value
return commandline_state.clInput.value
}
/** @hidden **/
export function editor_function(fn_name, ...args) {
let result = Promise.resolve([])
if (tri_editor[fn_name]) {
tri_editor[fn_name](commandline_state.clInput, ...args)
result = refresh_completions(commandline_state.clInput.value)
} else {
// The user is using the command line so we can't log message there
// logger.error(`No editor function named ${fn_name}!`)
console.error(`No editor function named ${fn_name}!`)
}
return result
}
import * as SELF from "@src/commandline_frame"
Messaging.addListener("commandline_frame", Messaging.attributeCaller(SELF))
import { getCommandlineFns } from "@src/lib/commandline_cmds"
import { KeyEventLike } from "./lib/keyseq"
commandline_state.fns = getCommandlineFns(commandline_state)
Messaging.addListener("commandline_cmd", Messaging.attributeCaller(commandline_state.fns))
// Listen for statistics from the commandline iframe and send them to
// the background for collection. Attach the observer to the window
// object since there's apparently a bug that causes performance
// observers to be GC'd even if they're still the target of a
// callback.
; (window as any).tri = Object.assign(window.tri || {}, {
perfObserver: perf.listenForCounters(),
})

View file

@ -11,12 +11,11 @@ Concrete completion classes have been moved to src/completions/.
*/
import * as Fuse from "fuse.js"
import { enumerate } from "./itertools"
import { toNumber } from "./convert"
import * as config from "./config"
import * as aliases from "./aliases"
import { enumerate } from "@src/lib/itertools"
import { toNumber } from "@src/lib/convert"
import * as aliases from "@src/lib/aliases"
export const DEFAULT_FAVICON = browser.extension.getURL(
export const DEFAULT_FAVICON = browser.runtime.getURL(
"static/defaultFavicon.svg",
)
@ -36,15 +35,21 @@ export abstract class CompletionSource {
node: HTMLElement
public completion: string
protected prefixes: string[] = []
protected lastFocused: CompletionOption
private _state: OptionState
private _prevState: OptionState
constructor(prefixes) {
let commands = aliases.getCmdAliasMapping()
const commands = aliases.getCmdAliasMapping()
// Now, for each prefix given as argument, add it to the completionsource's prefix list and also add any alias it has
prefixes.map(p => p.trim()).forEach(p => {
this.prefixes.push(p)
if (commands[p]) this.prefixes = this.prefixes.concat(commands[p])
})
prefixes
.map(p => p.trim())
.forEach(p => {
this.prefixes.push(p)
if (commands[p])
this.prefixes = this.prefixes.concat(commands[p])
})
// Not sure this is necessary but every completion source has it
this.prefixes = this.prefixes.map(p => p + " ")
@ -53,8 +58,6 @@ export abstract class CompletionSource {
/** Update [[node]] to display completions relevant to exstr */
public abstract filter(exstr: string): Promise<void>
private _state: OptionState
/** Control presentation of Source */
set state(newstate: OptionState) {
switch (newstate) {
@ -66,6 +69,7 @@ export abstract class CompletionSource {
this.node.classList.add("hidden")
break
}
this._prevState = this._state
this._state = newstate
}
@ -73,11 +77,21 @@ export abstract class CompletionSource {
return this._state
}
shouldRefresh() {
// A completion source should be refreshed if it is not hidden or if it just became hidden
return this._state !== "hidden" || this.state !== this._prevState
}
abstract next(inc?: number): boolean
prev(inc = 1): boolean {
return this.next(-1 * inc)
}
deselect() {
this.completion = undefined
if (this.lastFocused !== undefined) this.lastFocused.state = "normal"
}
}
// Default classes
@ -119,7 +133,7 @@ export interface CompletionOptionFuse extends CompletionOptionHTML {
fuseKeys: any[]
}
export type ScoredOption = {
export interface ScoredOption {
index: number
option: CompletionOptionFuse
score: number
@ -128,10 +142,21 @@ export type ScoredOption = {
export abstract class CompletionSourceFuse extends CompletionSource {
public node
public options: CompletionOptionFuse[]
protected lastExstr: string
protected lastFocused: CompletionOption
protected optionContainer = html`<table class="optionContainer">`
fuseOptions: Fuse.FuseOptions<any> = {
keys: ["fuseKeys"],
shouldSort: true,
id: "index",
includeScore: true,
}
// PERF: Could be expensive not to cache Fuse()
// yeah, it was.
fuse = undefined
protected lastExstr: string
protected optionContainer = html`<table class="optionContainer"></table>`
constructor(prefixes, className: string, title?: string) {
super(prefixes)
@ -150,7 +175,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
public async filter(exstr: string) {
this.lastExstr = exstr
await this.onInput(exstr)
await this.updateChain()
return this.updateChain()
}
updateChain(exstr = this.lastExstr, options = this.options) {
@ -188,7 +213,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
select(option: CompletionOption) {
if (this.lastExstr !== undefined && option !== undefined) {
const [prefix, _] = this.splitOnPrefix(this.lastExstr)
const [prefix] = this.splitOnPrefix(this.lastExstr)
this.completion = prefix + option.value
option.state = "focused"
this.lastFocused = option
@ -197,11 +222,6 @@ export abstract class CompletionSourceFuse extends CompletionSource {
}
}
deselect() {
this.completion = undefined
if (this.lastFocused != undefined) this.lastFocused.state = "normal"
}
splitOnPrefix(exstr: string) {
for (const prefix of this.prefixes) {
if (exstr.startsWith(prefix)) {
@ -212,27 +232,15 @@ export abstract class CompletionSourceFuse extends CompletionSource {
return [undefined, undefined]
}
fuseOptions = {
keys: ["fuseKeys"],
shouldSort: true,
id: "index",
includeScore: true,
}
// PERF: Could be expensive not to cache Fuse()
// yeah, it was.
fuse = undefined
/** Rtn sorted array of {option, score} */
scoredOptions(query: string, options = this.options): ScoredOption[] {
let searchThis = this.options.map((elem, index) => {
const searchThis = this.options.map((elem, index) => {
return { index, fuseKeys: elem.fuseKeys }
})
this.fuse = new Fuse(searchThis, this.fuseOptions)
return this.fuse.search(query).map(res => {
let result = res as any
return this.fuse.search(query).map(result => {
// console.log(result, result.item, query)
let index = toNumber(result.item)
const index = toNumber(result.item)
return {
index,
option: this.options[index],
@ -247,7 +255,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
focus the best match.
*/
setStateFromScore(scoredOpts: ScoredOption[], autoselect = false) {
let matches = scoredOpts.map(res => res.index)
const matches = scoredOpts.map(res => res.index)
for (const [index, option] of enumerate(this.options)) {
if (matches.includes(index)) option.state = "normal"
@ -275,7 +283,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
for (const option of this.options) {
/* newContainer.appendChild(option.html) */
if (option.state != "hidden")
if (option.state !== "hidden")
this.optionContainer.appendChild(option.html)
}
@ -287,13 +295,13 @@ export abstract class CompletionSourceFuse extends CompletionSource {
}
next(inc = 1) {
if (this.state != "hidden") {
let visopts = this.options.filter(o => o.state != "hidden")
let currind = visopts.findIndex(o => o.state == "focused")
if (this.state !== "hidden") {
const visopts = this.options.filter(o => o.state !== "hidden")
const currind = visopts.findIndex(o => o.state === "focused")
this.deselect()
// visopts.length + 1 because we want an empty completion at the end
let max = visopts.length + 1
let opt = visopts[(currind + inc + max) % max]
const max = visopts.length + 1
const opt = visopts[(currind + inc + max) % max]
if (opt) this.select(opt)
return true
} else return false
@ -301,47 +309,3 @@ export abstract class CompletionSourceFuse extends CompletionSource {
}
// }}}
// {{{ UNUSED: MANAGING ASYNC CHANGES
/** If first to modify epoch, commit change. May want to change epoch after commiting. */
async function commitIfCurrent(
epochref: any,
asyncFunc: Function,
commitFunc: Function,
...args: any[]
): Promise<any> {
// I *think* sync stuff in here is guaranteed to happen immediately after
// being called, up to the first await, despite this being an async
// function. But I don't know. Should check.
const epoch = epochref
const res = await asyncFunc(...args)
if (epoch === epochref) return commitFunc(res)
else console.error(new Error("Update failed: epoch out of date!"))
}
/** Indicate changes to completions we would like.
This will probably never be used for original designed purpose.
*/
function updateCompletions(filter: string, sources: CompletionSource[]) {
for (let [index, source] of enumerate(sources)) {
// Tell each compOpt to filter, and if they finish fast enough they:
// 0. Leave a note for any siblings that they got here first
// 1. Take over their parent's slot in compOpts
// 2. Update their display
commitIfCurrent(
source.obsolete, // Flag/epoch
source.filter, // asyncFunc
childSource => {
// commitFunc
source.obsolete = true
sources[index] = childSource
childSource.activate()
},
filter, // argument to asyncFunc
)
}
}
// }}}

View file

@ -1,5 +1,5 @@
import { browserBg } from "../lib/webext"
import * as Completions from "../completions"
import * as Completions from "@src/completions"
import * as providers from "@src/completions/providers"
class BmarkCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
@ -17,18 +17,15 @@ class BmarkCompletionOption extends Completions.CompletionOptionHTML
// Push properties we want to fuzmatch on
this.fuseKeys.push(bmark.title, bmark.url)
// Create HTMLElement
// need to download favicon
const favIconUrl = Completions.DEFAULT_FAVICON
// const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
this.html = html`<tr class="BmarkCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td class="icon"></td>
<td class="title">${bmark.title}</td>
<td class="content"><a class="url" target="_blank" href=${
bmark.url
}>${bmark.url}</a></td>
</tr>`
<td class="prefix">${"".padEnd(2)}</td>
<td class="title">${bmark.title}</td>
<td class="content">
<a class="url" target="_blank" href=${bmark.url}
>${bmark.url}</a
>
</td>
</tr>`
}
}
@ -43,7 +40,8 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
public async filter(exstr: string) {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
let [prefix, query] = this.splitOnPrefix(exstr)
let option = ""
// Hide self and stop if prefixes don't match
if (prefix) {
@ -56,12 +54,23 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
return
}
if (query.startsWith("-t ")) {
option = "-t "
query = query.slice(3)
}
if (query.startsWith("-c")) {
const args = query.split(" ")
option += args.slice(0, 2).join(" ")
option += " ";
query = args.slice(2).join(" ")
}
this.completion = undefined
this.options = (await this.scoreOptions(query, 10)).map(
page => new BmarkCompletionOption(page.url, page),
this.options = (await providers.getBookmarks(query)).slice(0, 10).map(
page => new BmarkCompletionOption(option + page.url, page),
)
this.updateChain()
return this.updateChain()
}
updateChain() {
@ -69,30 +78,14 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
this.updateDisplay()
return this.updateDisplay()
}
onInput() {}
private async scoreOptions(query: string, n: number) {
// Search bookmarks, dedupe and sort by frecency
let bookmarks = await browserBg.bookmarks.search({ query })
bookmarks = bookmarks.filter(b => {
try {
return new URL(b.url)
} catch (e) {
return false
}
})
bookmarks.sort((a, b) => b.dateAdded - a.dateAdded)
return bookmarks.slice(0, n)
}
select(option: Completions.CompletionOption) {
if (this.lastExstr !== undefined && option !== undefined) {
this.completion = "open " + option.value
this.completion = "bmarks " + option.value
option.state = "focused"
this.lastFocused = option
} else {

View file

@ -1,28 +1,21 @@
import * as Completions from "../completions"
import { typeToSimpleString } from "../metadata"
import * as Metadata from "../.metadata.generated"
import state from "../state"
import * as config from "../config"
import * as aliases from "../aliases"
import * as Completions from "@src/completions"
import * as Metadata from "@src/.metadata.generated"
import * as config from "@src/lib/config"
import * as aliases from "@src/lib/aliases"
class ExcmdCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(
public value: string,
public ttype: string = "",
public documentation: string = "",
) {
constructor(public value: string, public documentation: string = "") {
super()
this.fuseKeys.push(this.value)
// Create HTMLElement
this.html = html`<tr class="ExcmdCompletionOption option">
<td class="excmd">${value}</td>
<td class="documentation">${documentation}</td>
</tr>`
<td class="excmd">${value}</td>
<td class="documentation">${documentation}</td>
</tr>`
}
// <td class="type">${ttype}</td>
}
export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
@ -35,8 +28,13 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
this._parent.appendChild(this.node)
}
async filter(exstr) {
this.lastExstr = exstr
return this.onInput(exstr)
}
async onInput(exstr) {
await this.updateOptions(exstr)
return this.updateOptions(exstr)
}
updateChain(exstr = this.lastExstr, options = this.options) {
@ -46,49 +44,58 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
this.updateDisplay()
}
private async updateOptions(exstr?: string) {
if (!exstr) exstr = ""
this.lastExstr = exstr
let fns = Metadata.everything["src/excmds.ts"].functions
this.options = (await this.scoreOptions(
Object.keys(fns).filter(f => f.startsWith(exstr)),
)).map(f => {
let t = ""
if (fns[f].type) t = typeToSimpleString(fns[f].type)
return new ExcmdCompletionOption(f, t, fns[f].doc)
})
select(option: ExcmdCompletionOption) {
this.completion = option.value
option.state = "focused"
this.lastFocused = option
}
let exaliases = config.get("exaliases")
for (let alias of Object.keys(exaliases).filter(a =>
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
super.setStateFromScore(scoredOpts, false)
}
private async updateOptions(exstr = "") {
this.lastExstr = exstr
const excmds = Metadata.everything.getFile("src/excmds.ts")
if (!excmds) return
const fns = excmds.getFunctions()
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
this.options = this.scoreOptions(
fns
.filter(([name, fn]) => !fn.hidden && name.startsWith(exstr))
.map(([name, fn]) => new ExcmdCompletionOption(name, fn.doc)),
)
// Also add aliases to possible completions
const exaliases = Object.keys(config.get("exaliases")).filter(a =>
a.startsWith(exstr),
)) {
let cmd = aliases.expandExstr(alias)
if (fns[cmd]) {
this.options = this.options.concat(
)
for (const alias of exaliases) {
const cmd = aliases.expandExstr(alias)
const fn = excmds.getFunction(cmd)
if (fn) {
this.options.push(
new ExcmdCompletionOption(
alias,
fns[cmd].type ? typeToSimpleString(fns[cmd].type) : "",
`Alias for \`${cmd}\`. ${fns[cmd].doc}`,
`Alias for \`${cmd}\`. ${fn.doc}`,
),
)
} else {
// This can happen when the alias is a composite command or a command with arguments. We can't display type or doc because we don't know what parameter the alias takes or what it does.
this.options = this.options.concat(
new ExcmdCompletionOption(
alias,
"",
`Alias for \`${cmd}\`.`,
),
// This can happen when the alias is a composite command or a command with arguments. We can't display doc because we don't know what parameter the alias takes or what it does.
this.options.push(
new ExcmdCompletionOption(alias, `Alias for \`${cmd}\`.`),
)
}
}
this.options.forEach(o => (o.state = "normal"))
this.updateChain()
return this.updateChain()
}
private async scoreOptions(exstrs: string[]) {
return exstrs.sort()
private scoreOptions(options: ExcmdCompletionOption[]) {
return options.sort((o1, o2) => o1.value.localeCompare(o2.value))
// Too slow with large profiles
// let histpos = state.cmdHistory.map(s => s.split(" ")[0]).reverse()
@ -104,14 +111,4 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
// return posa < posb ? -1 : 1
// })
}
select(option: ExcmdCompletionOption) {
this.completion = option.value
option.state = "focused"
this.lastFocused = option
}
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
super.setStateFromScore(scoredOpts, false)
}
}

View file

@ -0,0 +1,71 @@
import * as Extensions from "@src/lib/extension_info"
import * as Completions from "@src/completions"
class ExtensionsCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public name: string, public optionsUrl: string) {
super()
this.fuseKeys.push(this.name)
this.html = html`<tr class="option">
<td class="title">${name}</td>
</tr>`
}
}
export class ExtensionsCompletionSource extends Completions.CompletionSourceFuse {
public options: ExtensionsCompletionOption[]
constructor(private _parent) {
super(
["extoptions"],
"ExtensionsCompletionSource",
"Extension options",
)
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
if (prefix) {
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
const extensions = await Extensions.listExtensions()
this.options = this.scoreOptions(
extensions
.filter(extension => extension.name.startsWith(query))
.map(extension => new ExtensionsCompletionOption(extension.name, extension.optionsUrl))
)
return this.updateChain()
}
onInput() {}
updateChain() {
this.options.forEach(option => (option.state = "normal"))
return this.updateDisplay()
}
select(option: ExtensionsCompletionOption) {
this.completion = "extoptions " + option.name
option.state = "focused"
this.lastFocused = option
}
private scoreOptions(options: ExtensionsCompletionOption[]) {
return options.sort((o1, o2) => o1.name.localeCompare(o2.name))
}
}

View file

@ -0,0 +1,75 @@
import * as Completions from "@src/completions"
import * as Native from "@src/lib/native"
class FileSystemCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public value: string) {
super()
this.fuseKeys = [value]
this.html = html`<tr class="FileSystemCompletionOption option">
<td class="value">${value}</td>
</tr>`
}
}
export class FileSystemCompletionSource extends Completions.CompletionSourceFuse {
public options: FileSystemCompletionOption[]
constructor(private _parent) {
super(["saveas", "source"], "FileSystemCompletionSource", "FileSystem")
this._parent.appendChild(this.node)
}
public async onInput(exstr) {
return this.filter(exstr)
}
public async filter(exstr: string) {
if (!exstr || exstr.indexOf(" ") === -1) {
this.state = "hidden"
return
}
let [cmd, path] = this.splitOnPrefix(exstr)
if (cmd === undefined) {
this.state = "hidden"
return
}
if (!path) path = "."
if (!["/", "$", "~", "."].find(s => path.startsWith(s))) {
// If the path doesn't start with a special character, it is relative to the native messenger, thus use "." as starting point
// Does this work on windows?
path = "./" + path
}
// Update lastExstr because we modified the path and scoreOptions uses that in order to assign scores
this.lastExstr = cmd + path
let req
try {
req = await Native.listDir(path)
} catch (e) {
// Failing silently because we can't nativegate (the user is typing stuff in the commandline)
this.state = "hidden"
return
}
if (req.isDir) {
if (!path.endsWith(req.sep)) path += req.sep
} else {
path = path.substring(0, path.lastIndexOf("/") + 1)
}
this.options = req.files.map(
p => new FileSystemCompletionOption(path + p),
)
this.state = "normal"
return this.updateChain()
}
}

116
src/completions/Find.ts Normal file
View file

@ -0,0 +1,116 @@
import { activeTabId } from "@src/lib/webext"
import * as Messaging from "@src/lib/messaging"
import * as Completions from "../completions"
import * as config from "@src/lib/config"
class FindCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(m, reverse = false) {
super()
this.value =
(reverse ? "-? " : "") + ("-: " + m.index) + " " + m.rangeData.text
this.fuseKeys.push(m.rangeData.text)
// Create HTMLElement
this.html = html`<tr class="FindCompletionOption option">
<td class="content">${m.precontext}<span class="match">${
m.rangeData.text
}</span>${m.postcontext}</td>
</tr>`
}
}
export class FindCompletionSource extends Completions.CompletionSourceFuse {
public options: FindCompletionOption[]
public prevCompletion = null
public completionCount = 0
private startingPosition = 0
constructor(private _parent) {
super(["find "], "FindCompletionSource", "Matches")
this.startingPosition = window.pageYOffset
this._parent.appendChild(this.node)
}
async onInput(exstr) {
const id = this.completionCount++
// If there's already a promise being executed, wait for it to finish
await this.prevCompletion
// Since we might have awaited for this.prevCompletion, we don't have a guarantee we're the last completion the user asked for anymore
if (id === this.completionCount - 1) {
// If we are the last completion
this.prevCompletion = this.updateOptions(exstr)
await this.prevCompletion
}
}
// Overriding this function is important, the default one has a tendency to hide options when you don't expect it
setStateFromScore(scoredOpts, autoselect) {
this.options.forEach(o => (o.state = "normal"))
}
private async updateOptions(exstr?: string) {
if (!exstr) return
// Flag parsing because -? should reverse completions
const tokens = exstr.split(" ")
const flagpos = tokens.indexOf("-?")
const reverse = flagpos >= 0
if (reverse) {
tokens.splice(flagpos, 1)
}
const query = tokens.slice(1).join(" ")
const minincsearchlen = await config.getAsync("minincsearchlen")
// No point if continuing if the user hasn't started searching yet
if (query.length < minincsearchlen) return
let findresults = await config.getAsync("findresults")
const incsearch = (await config.getAsync("incsearch")) === "true"
if (findresults === 0 && !incsearch) return
let incsearchonly = false
if (findresults === 0) {
findresults = 1
incsearchonly = true
}
// Note: the use of activeTabId here might break completions if the user starts searching for a pattern in a really big page and then switches to another tab.
// Getting the tabId should probably be done in the constructor but you can't have async constructors.
const tabId = await activeTabId()
const findings = await Messaging.messageTab(
tabId,
"finding_content",
"find",
[query, findresults, reverse],
)
// If the search was successful
if (findings.length > 0) {
// Get match context
const len = await config.getAsync("findcontextlen")
const matches = await Messaging.messageTab(
tabId,
"finding_content",
"getMatches",
[findings, len],
)
if (incsearch)
Messaging.messageTab(tabId, "finding_content", "jumpToMatch", [
query,
false,
0,
])
if (!incsearchonly) {
this.options = matches.map(
m => new FindCompletionOption(m, reverse),
)
this.updateChain(exstr, this.options)
}
}
}
}

90
src/completions/Guiset.ts Normal file
View file

@ -0,0 +1,90 @@
import * as Completions from "@src/completions"
import { potentialRules, metaRules } from "@src/lib/css_util"
class GuisetCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public value: string, displayValue: string) {
super()
this.fuseKeys.push(value)
this.html = html`<tr class="GuisetCompletionOption option">
<td class="value">${displayValue}</td>
</tr>`
}
}
export class GuisetCompletionSource extends Completions.CompletionSourceFuse {
public options: GuisetCompletionOption[]
constructor(private _parent) {
super(["guiset", "guiset_quiet"], "GuisetCompletionSource", "Guiset")
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
this.completion = undefined
let ruleName = ""
let subRule = ""
if (query) {
const args = query.trim().split(" ")
ruleName = args[0] || ""
subRule = args[1] || ""
}
this.options = []
if (metaRules[ruleName]) {
this.options = this.options.concat(
Object.keys(metaRules[ruleName])
.filter(s => s.startsWith(subRule))
.map(
s => new GuisetCompletionOption(`${ruleName} ${s}`, s),
),
)
}
if (potentialRules[ruleName]) {
this.options = this.options.concat(
Object.keys(potentialRules[ruleName].options)
.filter(s => s.startsWith(subRule))
.map(
s => new GuisetCompletionOption(`${ruleName} ${s}`, s),
),
)
}
if (this.options.length === 0) {
this.options = Object.keys(metaRules)
.concat(Object.keys(potentialRules))
.filter(s => s.startsWith(ruleName))
.map(s => new GuisetCompletionOption(s, s))
}
return this.updateChain()
}
updateChain() {
// Options are pre-trimmed to the right length.
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
return this.updateDisplay()
}
onInput(arg) {
return this.filter(arg)
}
}

148
src/completions/Help.ts Normal file
View file

@ -0,0 +1,148 @@
import * as Completions from "@src/completions"
import * as Metadata from "@src/.metadata.generated"
import * as aliases from "@src/lib/aliases"
import * as config from "@src/lib/config"
class HelpCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public name: string, doc: string, flag: string) {
super()
this.value = `${flag} ${name}`
this.html = html`<tr class="HelpCompletionOption option">
<td class="name">${name}</td>
<td class="doc">${doc}</td>
</tr>`
}
}
export class HelpCompletionSource extends Completions.CompletionSourceFuse {
public options: HelpCompletionOption[]
constructor(private _parent) {
super(["help"], "HelpCompletionSource", "Help")
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
this.lastExstr = exstr
this.completion = undefined
const [prefix, query] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
const file = Metadata.everything.getFile("src/lib/config.ts")
const default_config = file.getClass("default_config")
const excmds = Metadata.everything.getFile("src/excmds.ts")
const fns = excmds.getFunctions()
const settings = config.get()
const exaliases = settings.exaliases
const bindings = settings.nmaps
if (fns === undefined || exaliases === undefined || bindings === undefined) {
return
}
const flags = {
"-a": (options, query) =>
options.concat(
Object.keys(exaliases)
.filter(alias => alias.startsWith(query))
.map(alias => {
const cmd = aliases.expandExstr(alias)
const doc = (excmds.getFunction(cmd) || {} as any).doc || ""
return new HelpCompletionOption(
alias,
`Alias for \`${cmd}\`. ${doc}`,
"-a",
)
}),
),
"-b": (options, query) =>
options.concat(
Object.keys(bindings)
.filter(binding => binding.startsWith(query))
.map(
binding =>
new HelpCompletionOption(
binding,
`Normal mode binding for \`${
bindings[binding]
}\``,
"-b",
),
),
),
"-e": (options, query) =>
options.concat(
fns
.filter(
([name, fn]) =>
!fn.hidden && name.startsWith(query),
)
.map(
([name, fn]) =>
new HelpCompletionOption(
name,
`Excmd. ${fn.doc}`,
"-e",
),
),
),
"-s": (options, query) =>
options.concat(
Object.keys(settings)
.filter(x => x.startsWith(query))
.map(setting => {
const member = default_config.getMember(setting)
let doc = ""
if (member !== undefined) {
doc = member.doc
}
return new HelpCompletionOption(
setting,
`Setting. ${doc}`,
"-s",
)
}),
),
}
const args = query.split(" ")
let opts = []
if (Object.keys(flags).includes(args[0])) {
opts = flags[args[0]](opts, args.slice(1).join(" "))
} else {
opts = Object.keys(flags).reduce(
(acc, curFlag) => flags[curFlag](acc, query),
[],
)
}
this.options = opts
this.options.sort((compopt1, compopt2) =>
compopt1.name.localeCompare(compopt2.name),
)
return this.updateChain()
}
updateChain() {
// Options are pre-trimmed to the right length.
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
return this.updateDisplay()
}
onInput() {}
}

View file

@ -1,6 +1,6 @@
import * as Completions from "../completions"
import * as config from "../config"
import { browserBg } from "../lib/webext"
import * as Completions from "@src/completions"
import * as config from "@src/lib/config"
import * as providers from "@src/completions/providers"
class HistoryCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
@ -14,19 +14,17 @@ class HistoryCompletionOption extends Completions.CompletionOptionHTML
// Push properties we want to fuzmatch on
this.fuseKeys.push(page.title, page.url) // weight by page.visitCount
// Create HTMLElement
// need to download favicon
const favIconUrl = Completions.DEFAULT_FAVICON
// const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
this.html = html`<tr class="HistoryCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td class="icon"></td>
<td class="title">${page.title}</td>
<td class="content"><a class="url" target="_blank" href=${
page.url
}>${page.url}</a></td>
</tr>`
<td class="prefix">${"".padEnd(2)}</td>
<td class="title">${page.title}</td>
<td class="content">
<a class="url" target="_blank" href=${page.url}
>${page.url}</a
>
</td>
</tr>`
}
}
@ -37,13 +35,14 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
super(
["open", "tabopen", "winopen"],
"HistoryCompletionSource",
"History",
"History and bookmarks",
)
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
const prevStr = this.lastExstr
this.lastExstr = exstr
let [prefix, query] = this.splitOnPrefix(exstr)
let options = ""
@ -61,85 +60,54 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
// Ignoring command-specific arguments
// It's terrible but it's ok because it's just a stopgap until an actual commandline-parsing API is implemented
if (prefix == "tabopen ") {
if (prefix === "tabopen ") {
if (query.startsWith("-c")) {
let args = query.split(" ")
const args = query.split(" ")
options = args.slice(0, 2).join(" ")
query = args.slice(2).join(" ")
}
if (query.startsWith("-b")) {
let args = query.split(" ")
const args = query.split(" ")
options = args.slice(0, 1).join(" ")
query = args.slice(1).join(" ")
}
} else if (prefix == "winopen ") {
if (query.startsWith("-private")) {
options = "-private"
query = query.substring(options.length)
}
} else if (prefix === "winopen " && query.startsWith("-private")) {
options = "-private"
query = query.substring(options.length)
}
options += options ? " " : ""
this.completion = undefined
// Options are pre-trimmed to the right length.
this.options = (await this.scoreOptions(query, 10)).map(
page => new HistoryCompletionOption(options + page.url, page),
)
this.updateChain()
// Deselect any selected, but remember what they were.
const lastFocused = this.lastFocused
this.deselect()
// Set initial state to normal, unless the option was selected a moment
// ago, then reselect it so that users don't lose their selections.
this.options.forEach(option => option.state = "normal")
for (const option of this.options) {
if (lastFocused !== undefined && lastFocused.value === option.value && prevStr.length <= exstr.length) {
this.select(option)
break
}
}
return this.updateDisplay()
}
updateChain() {
// Options are pre-trimmed to the right length.
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
this.updateDisplay()
}
updateChain() {}
onInput() {}
private frecency(item: browser.history.HistoryItem) {
// Doesn't actually care about recency yet.
return item.visitCount * -1
}
private async scoreOptions(query: string, n: number) {
const newtab = browser.runtime.getManifest()["chrome_url_overrides"]
.newtab
const newtaburl = browser.extension.getURL(newtab)
if (!query || config.get("historyresults") == 0) {
return (await browserBg.topSites.get())
.filter(page => page.url !== newtaburl)
.slice(0, n)
if (!query || config.get("historyresults") === 0) {
return (await providers.getTopSites()).slice(0, n)
} else {
// Search history, dedupe and sort by frecency
let history = await browserBg.history.search({
text: query,
maxResults: Number(config.get("historyresults")),
startTime: 0,
})
// Remove entries with duplicate URLs
const dedupe = new Map()
for (const page of history) {
if (page.url !== newtaburl) {
if (dedupe.has(page.url)) {
if (
dedupe.get(page.url).title.length <
page.title.length
) {
dedupe.set(page.url, page)
}
} else {
dedupe.set(page.url, page)
}
}
}
history = [...dedupe.values()]
history.sort((a, b) => this.frecency(a) - this.frecency(b))
return history.slice(0, n)
return (await providers.getCombinedHistoryBmarks(query)).slice(0, n)
}
}
}

View file

@ -0,0 +1,49 @@
import * as Completions from "@src/completions"
import * as Native from "@src/lib/native"
class PreferenceCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public value: string, public prefvalue: string) {
super()
this.fuseKeys.push(value)
this.html = html`<tr class="PreferenceCompletionOption option">
<td class="name">${value}</td>
<td class="value">${prefvalue}</td>
</tr>`
}
}
export class PreferenceCompletionSource extends Completions.CompletionSourceFuse {
public options: PreferenceCompletionOption[]
constructor(private _parent) {
super(["setpref"], "PreferenceCompletionSource", "Preference")
this._parent.appendChild(this.node)
}
public onInput(exstr: string) {
return this.filter(exstr)
}
public async filter(exstr: string) {
if (!exstr) {
this.state = "hidden"
return
}
const pref = this.splitOnPrefix(exstr)[1]
if (pref === undefined) {
this.state = "hidden"
return
}
this.lastExstr = exstr
const preferences = await Native.getPrefs()
this.options = Object.keys(preferences)
.filter(key => key.startsWith(pref))
.map(key => new PreferenceCompletionOption(key, preferences[key]))
if (this.options.length > 0) this.state = "normal"
return this.updateChain()
}
}

71
src/completions/Rss.ts Normal file
View file

@ -0,0 +1,71 @@
import * as Messaging from "@src/lib/messaging"
import * as Completions from "@src/completions"
class RssCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public url, public title, public type) {
super()
this.value = `${url} ${type} ${title}`
this.fuseKeys.push(url)
this.fuseKeys.push(title)
this.html = html`<tr class="RssCompletionOption option">
<td class="title">${title}</td>
<td class="content">
<a class="url" target="_blank" href=${url}>${url}</a>
</td>
<td class="type">${type}</td>
</tr>`
}
}
export class RssCompletionSource extends Completions.CompletionSourceFuse {
public options: RssCompletionOption[] = []
private shouldSetStateFromScore = true
constructor(private _parent) {
super(["rssexec"], "RssCompletionSource", "Feeds")
this.updateOptions()
this._parent.appendChild(this.node)
}
onInput(...whatever) {
return this.updateOptions(...whatever)
}
private async updateOptions(exstr = "") {
this.lastExstr = exstr
const [prefix] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
if (this.options.length < 1) {
this.options = (await Messaging.messageOwnTab(
"excmd_content",
"getRssLinks",
[],
)).map(link => {
const opt = new RssCompletionOption(
link.url,
link.title,
link.type,
)
opt.state = "normal"
return opt
})
}
return this.updateChain()
}
}

View file

@ -0,0 +1,98 @@
import { browserBg } from "@src/lib/webext.ts"
import * as Completions from "@src/completions"
function computeDate(session) {
let howLong = Math.round(
((new Date() as any) - session.lastModified) / 1000,
)
let qualifier = "s"
if (howLong > 60) {
qualifier = "m"
howLong = Math.round(howLong / 60)
if (howLong > 60) {
qualifier = "h"
howLong = Math.round(howLong / 60)
if (howLong > 24) {
qualifier = "d"
howLong = Math.round(howLong / 24)
}
}
}
return [howLong, qualifier]
}
function getTabInfo(session) {
let tab
let extraInfo
if (session.tab) {
tab = session.tab
extraInfo = tab.url
} else {
tab = session.window.tabs.sort(
(a, b) => b.lastAccessed - a.lastAccessed,
)[0]
const tabCount = session.window.tabs.length
if (tabCount < 2) {
extraInfo = tab.url
} else {
extraInfo = `${tabCount - 1} more tab${tabCount > 2 ? "s" : ""}.`
}
}
return [tab, extraInfo]
}
class SessionCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public session) {
super()
this.value = (session.tab || session.window).sessionId
const [howLong, qualifier] = computeDate(session)
const [tab, extraInfo] = getTabInfo(session)
this.fuseKeys.push(tab.title)
this.html = html`<tr class="SessionCompletionOption option">
<td class="type">${session.tab ? "T" : "W"}</td>
<td class="time">${howLong}${qualifier}</td>
<td class="icon"><img src="${tab.favIconUrl ||
Completions.DEFAULT_FAVICON}"/></td>
<td class="title">${tab.title}</td>
<td class="extraInfo">${extraInfo}</td>
</tr>`
}
}
export class SessionsCompletionSource extends Completions.CompletionSourceFuse {
public options: SessionCompletionOption[]
private shouldSetStateFromScore = true
constructor(private _parent) {
super(["undo"], "SessionCompletionSource", "sessions")
this.updateOptions()
this._parent.appendChild(this.node)
}
async onInput(exstr) {
return this.updateOptions(exstr)
}
private async updateOptions(exstr = "") {
this.lastExstr = exstr
const [prefix] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
const sessions = await browserBg.sessions.getRecentlyClosed()
this.options = sessions.map(s => new SessionCompletionOption(s))
}
}

View file

@ -1,8 +1,6 @@
import * as Completions from "../completions"
import * as config from "../config"
import { browserBg } from "../lib/webext"
import * as metadata from "../.metadata.generated"
import { typeToString } from "../metadata"
import * as Completions from "@src/completions"
import * as config from "@src/lib/config"
import * as metadata from "@src/.metadata.generated"
class SettingsCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
@ -14,11 +12,11 @@ class SettingsCompletionOption extends Completions.CompletionOptionHTML
) {
super()
this.html = html`<tr class="SettingsCompletionOption option">
<td class="title">${setting.name}</td>
<td class="content">${setting.value}</td>
<td class="type">${setting.type}</td>
<td class="doc">${setting.doc}</td>
</tr>`
<td class="title">${setting.name}</td>
<td class="content">${setting.value}</td>
<td class="type">${setting.type}</td>
<td class="doc">${setting.doc}</td>
</tr>`
}
}
@ -26,7 +24,11 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
public options: SettingsCompletionOption[]
constructor(private _parent) {
super(["set", "get", "unset"], "SettingsCompletionSource", "Settings")
super(
["set", "get", "unset", "seturl", "unseturl"],
"SettingsCompletionSource",
"Settings",
)
this._parent.appendChild(this.node)
}
@ -47,29 +49,45 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
return
}
let configmd =
metadata.everything["src/config.ts"].classes.default_config
let settings = config.get()
// Ignoring command-specific arguments
// It's terrible but it's ok because it's just a stopgap until an actual commandline-parsing API is implemented
// copy pasting code is fun and good
if (prefix === "seturl " || prefix === "unseturl ") {
const args = query.split(" ")
options = args.slice(0, 1).join(" ")
query = args.slice(1).join(" ")
}
options += options ? " " : ""
const file = metadata.everything.getFile("src/lib/config.ts")
const default_config = file.getClass("default_config")
const settings = config.get()
if (default_config === undefined || settings === undefined) {
return
}
this.options = Object.keys(settings)
.filter(x => x.startsWith(query))
.sort()
.map(setting => {
const md = default_config.getMember(setting)
let doc = ""
let type = ""
if (configmd[setting]) {
doc = configmd[setting].doc.join(" ")
type = typeToString(configmd[setting].type)
if (md !== undefined) {
doc = md.doc
type = md.type.toString()
}
return new SettingsCompletionOption(setting, {
return new SettingsCompletionOption(options + setting, {
name: setting,
value: JSON.stringify(settings[setting]),
doc: doc,
type: type,
doc,
type,
})
})
// this.options = [new SettingsCompletionOption("ok", {name: "ok", docs:""})]
this.updateChain()
return this.updateChain()
}
updateChain() {
@ -77,7 +95,7 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
this.updateDisplay()
return this.updateDisplay()
}
onInput() {}

View file

@ -1,7 +1,8 @@
import { enumerate } from "../itertools"
import * as Containers from "../lib/containers"
import * as Messaging from "../messaging"
import * as Completions from "../completions"
import * as Perf from "@src/perf"
import { browserBg } from "@src/lib/webext.ts"
import { enumerate } from "@src/lib/itertools"
import * as Containers from "@src/lib/containers"
import * as Completions from "@src/completions"
class BufferCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
@ -14,7 +15,7 @@ class BufferCompletionOption extends Completions.CompletionOptionHTML
container: browser.contextualIdentities.ContextualIdentity,
) {
super()
// Two character buffer properties prefix
// Two character tab properties prefix
let pre = ""
if (tab.active) pre += "%"
else if (isAlternative) {
@ -33,22 +34,24 @@ class BufferCompletionOption extends Completions.CompletionOptionHTML
const favIconUrl = tab.favIconUrl
? tab.favIconUrl
: Completions.DEFAULT_FAVICON
this.html = html`<tr class="BufferCompletionOption option container_${
container.color
} container_${container.icon} container_${container.name}">
<td class="prefix">${pre.padEnd(2)}</td>
<td class="container"></td>
<td class="icon"><img src="${favIconUrl}"/></td>
<td class="title">${tab.index + 1}: ${tab.title}</td>
<td class="content"><a class="url" target="_blank" href=${
tab.url
}>${tab.url}</a></td>
</tr>`
this.html = html`<tr class="BufferCompletionOption option container_${container.color} container_${container.icon} container_${container.name}"
>
<td class="prefix">${pre.padEnd(2)}</td>
<td class="container"></td>
<td class="icon"><img src="${favIconUrl}" /></td>
<td class="title">${tab.index + 1}: ${tab.title}</td>
<td class="content">
<a class="url" target="_blank" href=${tab.url}
>${tab.url}</a
>
</td>
</tr>`
}
}
export class BufferCompletionSource extends Completions.CompletionSourceFuse {
public options: BufferCompletionOption[]
private shouldSetStateFromScore = true
// TODO:
// - store the exstr and trigger redraws on user or data input without
@ -57,59 +60,31 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
constructor(private _parent) {
super(
["buffer", "tabclose", "tabdetach", "tabduplicate", "tabmove"],
["tab", "tabclose", "tabdetach", "tabduplicate", "tabmove"],
"BufferCompletionSource",
"Buffers",
"Tabs",
)
this.updateOptions()
this._parent.appendChild(this.node)
}
private async updateOptions(exstr?: string) {
/* console.log('updateOptions', this.optionContainer) */
const tabs: browser.tabs.Tab[] = await Messaging.message(
"commandline_background",
"currentWindowTabs",
)
const options = []
// Get alternative tab, defined as last accessed tab.
const alt = tabs.sort((a, b) => {
return a.lastAccessed < b.lastAccessed ? 1 : -1
})[1]
tabs.sort((a, b) => {
return a.index < b.index ? -1 : 1
})
for (const tab of tabs) {
options.push(
new BufferCompletionOption(
(tab.index + 1).toString(),
tab,
tab === alt,
await Containers.getFromId(tab.cookieStoreId),
),
)
}
this.completion = undefined
this.options = options
this.updateChain()
async onInput(exstr) {
// Schedule an update, if you like. Not very useful for tabs, but
// will be for other things.
return this.updateOptions(exstr)
}
async onInput(exstr) {
// Schedule an update, if you like. Not very useful for buffers, but
// will be for other things.
this.updateOptions()
async filter(exstr) {
this.lastExstr = exstr
return this.onInput(exstr)
}
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
super.setStateFromScore(scoredOpts, true)
super.setStateFromScore(scoredOpts, this.shouldSetStateFromScore)
}
/** Score with fuse unless query is a single # or looks like a buffer index */
/** Score with fuse unless query is a single # or looks like a tab index */
scoredOptions(
query: string,
options = this.options,
@ -147,4 +122,56 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
// If not yet returned...
return super.scoredOptions(query, options)
}
@Perf.measuredAsync
private async updateOptions(exstr = "") {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
// When the user is asking for tabmove completions, don't autoselect if the query looks like a relative move https://github.com/tridactyl/tridactyl/issues/825
this.shouldSetStateFromScore = !(
prefix === "tabmove " && query.match("^[+-][0-9]+$")
)
/* console.log('updateOptions', this.optionContainer) */
const tabs: browser.tabs.Tab[] = await browserBg.tabs.query({
currentWindow: true,
})
const options = []
// Get alternative tab, defined as last accessed tab.
tabs.sort((a, b) => (b.lastAccessed - a.lastAccessed))
const alt = tabs[1]
tabs.sort((a, b) => (a.index - b.index))
for (const tab of tabs) {
options.push(
new BufferCompletionOption(
(tab.index + 1).toString(),
tab,
tab === alt,
await Containers.getFromId(tab.cookieStoreId),
),
)
}
this.completion = undefined
this.options = options
if (query && query.trim().length > 0) {
this.setStateFromScore(this.scoredOptions(query))
} else {
this.options.forEach(option => (option.state = "normal"))
}
return this.updateDisplay()
}
}

View file

@ -1,9 +1,9 @@
import { browserBg } from "../lib/webext"
import * as Containers from "../lib/containers"
import * as Messaging from "../messaging"
import * as Completions from "../completions"
import * as Perf from "@src/perf"
import { browserBg } from "@src/lib/webext"
import * as Containers from "@src/lib/containers"
import * as Completions from "@src/completions"
class BufferAllCompletionOption extends Completions.CompletionOptionHTML
class TabAllCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(
@ -21,35 +21,40 @@ class BufferAllCompletionOption extends Completions.CompletionOptionHTML
const favIconUrl = tab.favIconUrl
? tab.favIconUrl
: Completions.DEFAULT_FAVICON
this.html = html`<tr class="BufferAllCompletionOption option container_${
container.color
} container_${container.icon} container_${container.name} ${
incognito ? "incognito" : ""
}">
<td class="prefix"></td>
<td class="privatewindow"></td>
<td class="container"></td>
<td class="icon"><img src="${favIconUrl}"/></td>
<td class="title">${this.value}: ${tab.title}</td>
<td class="content"><a class="url" target="_blank" href=${
tab.url
}>${tab.url}</a></td>
</tr>`
this.html = html`<tr class="BufferAllCompletionOption option container_${container.color} container_${container.icon} container_${container.name} ${incognito
? "incognito"
: ""}"
>
<td class="prefix"></td>
<td class="privatewindow"></td>
<td class="container"></td>
<td class="icon"><img src="${favIconUrl}" /></td>
<td class="title">${this.value}: ${tab.title}</td>
<td class="content">
<a class="url" target="_blank" href=${tab.url}
>${tab.url}</a
>
</td>
</tr>`
}
}
export class BufferAllCompletionSource extends Completions.CompletionSourceFuse {
public options: BufferAllCompletionOption[]
export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
public options: TabAllCompletionOption[]
constructor(private _parent) {
super(["bufferall"], "BufferAllCompletionSource", "All Buffers")
super(["taball"], "TabAllCompletionSource", "All Tabs")
this.updateOptions()
this._parent.appendChild(this.node)
}
async onInput(exstr) {
await this.updateOptions()
return this.updateOptions(exstr)
}
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
super.setStateFromScore(scoredOpts, true)
}
/**
@ -57,23 +62,35 @@ export class BufferAllCompletionSource extends Completions.CompletionSourceFuse
*/
private async getWindows() {
const windows = await browserBg.windows.getAll()
const response: {[windowId: number]: browser.windows.Window} = {}
windows.forEach(win => response[win.id] = win)
const response: { [windowId: number]: browser.windows.Window } = {}
windows.forEach(win => (response[win.id] = win))
return response
}
private async updateOptions(exstr?: string) {
const tabsPromise = Messaging.message(
"commandline_background",
"allWindowTabs",
)
@Perf.measuredAsync
private async updateOptions(exstr = "") {
this.lastExstr = exstr
const [prefix] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
const tabsPromise = browserBg.tabs.query({})
const windowsPromise = this.getWindows()
const [tabs, windows] = await Promise.all([tabsPromise, windowsPromise])
const options = []
tabs.sort((a, b) => {
if (a.windowId == b.windowId) return a.index - b.index
if (a.windowId === b.windowId) return a.index - b.index
return a.windowId - b.windowId
})
@ -82,12 +99,12 @@ export class BufferAllCompletionSource extends Completions.CompletionSourceFuse
let lastId = 0
let winindex = 0
for (const tab of tabs) {
if (lastId != tab.windowId) {
if (lastId !== tab.windowId) {
lastId = tab.windowId
winindex += 1
}
options.push(
new BufferAllCompletionOption(
new TabAllCompletionOption(
tab.id.toString(),
tab,
winindex,
@ -99,10 +116,6 @@ export class BufferAllCompletionSource extends Completions.CompletionSourceFuse
this.completion = undefined
this.options = options
this.updateChain()
}
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
super.setStateFromScore(scoredOpts, true)
return this.updateChain()
}
}

73
src/completions/Window.ts Normal file
View file

@ -0,0 +1,73 @@
import { browserBg } from "@src/lib/webext.ts"
import * as Completions from "@src/completions"
class WindowCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(win) {
super()
this.value = win.id
this.fuseKeys.push(`${win.title}`)
this.fuseKeys.push(`${win.id}`)
// Create HTMLElement
this.html = html`<tr class="WindowCompletionOption option ${
win.incognito ? "incognito" : ""
}">
<td class="privatewindow"></td>
<td class="id">${win.id}</td>
<td class="title">${win.title}</td>
<td class="tabcount">${win.tabs.length} tab${
win.tabs.length !== 1 ? "s" : ""
}</td>
</tr>`
}
}
export class WindowCompletionSource extends Completions.CompletionSourceFuse {
public options: WindowCompletionOption[]
constructor(private _parent) {
super(["winclose"], "WindowCompletionSource", "Windows")
this.updateOptions()
this._parent.appendChild(this.node)
}
async onInput(exstr) {
// Schedule an update, if you like. Not very useful for windows, but
// will be for other things.
return this.updateOptions(exstr)
}
async filter(exstr) {
this.lastExstr = exstr
return this.onInput(exstr)
}
private async updateOptions(exstr = "") {
this.lastExstr = exstr
const [prefix] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
this.options = (await browserBg.windows.getAll({ populate: true })).map(
win => {
const o = new WindowCompletionOption(win)
o.state = "normal"
return o
},
)
return this.updateDisplay()
}
}

View file

@ -0,0 +1,99 @@
import * as config from "@src/lib/config"
import { browserBg } from "@src/lib/webext"
export function newtaburl() {
// In the nonewtab version, this will return `null` and upset getURL.
// Ternary op below prevents the runtime error.
const newtab = (browser.runtime.getManifest()).chrome_url_overrides.newtab
return newtab !== null ? browser.runtime.getURL(newtab) : null
}
export async function getBookmarks(query: string) {
// Search bookmarks, dedupe and sort by most recent.
let bookmarks = await browserBg.bookmarks.search({ query })
// Remove folder nodes and bad URLs
bookmarks = bookmarks.filter(b => {
try {
return new URL(b.url)
} catch (e) {
return false
}
})
bookmarks.sort((a, b) => b.dateAdded - a.dateAdded)
// Remove duplicate bookmarks
const seen = new Map<string, string>()
bookmarks = bookmarks.filter(b => {
if (seen.get(b.title) === b.url)
return false
else {
seen.set(b.title, b.url)
return true
}
})
return bookmarks
}
function frecency(item: browser.history.HistoryItem) {
// Doesn't actually care about recency yet.
return item.visitCount * -1
}
export async function getHistory(query: string): Promise<browser.history.HistoryItem[]> {
// Search history, dedupe and sort by frecency
let history = await browserBg.history.search({
text: query,
maxResults: config.get("historyresults"),
startTime: 0,
})
// Remove entries with duplicate URLs
const dedupe = new Map()
for (const page of history) {
if (page.url !== newtaburl()) {
if (dedupe.has(page.url)) {
if (
dedupe.get(page.url).title.length <
page.title.length
) {
dedupe.set(page.url, page)
}
} else {
dedupe.set(page.url, page)
}
}
}
history = [...dedupe.values()]
history.sort((a, b) => frecency(a) - frecency(b))
return history
}
export async function getTopSites() {
return (await browserBg.topSites.get())
.filter(page => page.url !== newtaburl())
}
export async function getCombinedHistoryBmarks(query: string): Promise<Array<{title: string, url: string}>> {
const [history, bookmarks] = await Promise.all([
getHistory(query),
getBookmarks(query),
])
// Join records by URL, using the title from bookmarks by preference.
const combinedMap = new Map<string, any>(bookmarks.map(bmark => [
bmark.url, {title: bmark.title, url: bmark.url, bmark}
]))
history.forEach(page => {
if (combinedMap.has(page.url)) combinedMap.get(page.url).history = page
else combinedMap.set(page.url, {title: page.title, url: page.url, history: page})
})
const score = x => (x.history ? frecency(x.history) : 0) - (x.bmark ? config.get("bmarkweight") : 0)
return Array.from(combinedMap.values()).sort((a, b) => score(a) - score(b))
}

View file

@ -1,848 +0,0 @@
// Sketch
//
// Need an easy way of getting and setting settings
// If a setting is not set, the default should probably be returned.
// That probably means that binds etc. should be per-key?
//
// We should probably store all settings in memory, and only load from storage on startup and when we set it
//
// Really, we'd like a way of just letting things use the variables
//
/** # Tridactyl Configuration
*
* We very strongly recommend that you pretty much ignore this page and instead follow the link below DEFAULTS that will take you to our own source code which is formatted in a marginally more sane fashion.
*
*/
/** @hidden */
const CONFIGNAME = "userconfig"
/** @hidden */
const WAITERS = []
/** @hidden */
let INITIALISED = false
/** @hidden */
// make a naked object
function o(object) {
return Object.assign(Object.create(null), object)
}
/** @hidden */
// "Import" is a reserved word so this will have to do
function schlepp(settings) {
Object.assign(USERCONFIG, settings)
}
/** @hidden */
let USERCONFIG = o({})
/**
* This is the default configuration that Tridactyl comes with.
*
* You can change anything here using `set key1.key2.key3 value` or specific things any of the various helper commands such as `bind` or `command`.
*
*/
class default_config {
/**
* Internal version number Tridactyl uses to know whether it needs to update from old versions of the configuration.
*
* Changing this might do weird stuff.
*/
configversion = "0.0"
// Note to developers: When creating new <modifier-letter> maps, make sure to make the modifier uppercase (e.g. <C-a> instead of <c-a>) otherwise some commands might not be able to find them (e.g. `bind <c-a>`)
/**
* ignoremaps contain all of the bindings for "ignore mode".
*
* They consist of key sequences mapped to ex commands.
*/
ignoremaps = {
"<S-Insert>": "mode normal",
"<CA-Escape>": "mode normal",
"<CA-`>": "mode normal",
"<S-Escape>": "mode normal",
I: "mode normal",
}
/**
* inputmaps contain all of the bindings for "input mode".
*
* They consist of key sequences mapped to ex commands.
*/
inputmaps = {
"<Escape>": "composite unfocus | mode normal",
"<C-[>": "composite unfocus | mode normal",
"<C-i>": "editor",
"<Tab>": "focusinput -n",
"<S-Tab>": "focusinput -N",
"<CA-Escape>": "mode normal",
"<CA-`>": "mode normal",
"<C-^>": "buffer #",
}
/**
* imaps contain all of the bindings for "insert mode".
*
* They consist of key sequences mapped to ex commands.
*/
imaps = {
"<Escape>": "composite unfocus | mode normal",
"<C-[>": "composite unfocus | mode normal",
"<C-i>": "editor",
"<CA-Escape>": "mode normal",
"<CA-`>": "mode normal",
"<C-6>": "buffer #",
"<C-^>": "buffer #",
"<S-Escape>": "mode ignore",
}
/**
* nmaps contain all of the bindings for "normal mode".
*
* They consist of key sequences mapped to ex commands.
*/
nmaps = {
"<A-p>": "pin",
"<A-m>": "mute toggle",
"<F1>": "help",
o: "fillcmdline open",
O: "current_url open",
w: "fillcmdline winopen",
W: "current_url winopen",
t: "fillcmdline tabopen",
"]]": "followpage next",
"[[": "followpage prev",
"[c": "urlincrement -1",
"]c": "urlincrement 1",
"<C-x>": "urlincrement -1",
"<C-a>": "urlincrement 1",
T: "current_url tabopen",
yy: "clipboard yank",
ys: "clipboard yankshort",
yc: "clipboard yankcanon",
ym: "clipboard yankmd",
yt: "clipboard yanktitle",
gh: "home",
gH: "home true",
p: "clipboard open",
P: "clipboard tabopen",
j: "scrollline 10",
"<C-e>": "scrollline 10",
k: "scrollline -10",
"<C-y>": "scrollline -10",
h: "scrollpx -50",
l: "scrollpx 50",
G: "scrollto 100",
gg: "scrollto 0",
"<C-u>": "scrollpage -0.5",
"<C-d>": "scrollpage 0.5",
"<C-f>": "scrollpage 1",
"<C-b>": "scrollpage -1",
$: "scrollto 100 x",
// "0": "scrollto 0 x", // will get interpreted as a count
"^": "scrollto 0 x",
"<C-6>": "buffer #",
"<C-^>": "buffer #",
H: "back",
L: "forward",
"<C-o>": "jumpprev",
"<C-i>": "jumpnext",
d: "tabclose",
D: "composite tabprev; sleep 100; tabclose #",
gx0: "tabclosealltoleft",
gx$: "tabclosealltoright",
"<<": "tabmove -1",
">>": "tabmove +1",
u: "undo",
r: "reload",
R: "reloadhard",
gi: "focusinput -l",
"g;": "changelistjump -1",
gt: "tabnext_gt",
gT: "tabprev",
// "<c-n>": "tabnext_gt", // c-n is reserved for new window
// "<c-p>": "tabprev",
"g^": "tabfirst",
g0: "tabfirst",
g$: "tablast",
gr: "reader",
gu: "urlparent",
gU: "urlroot",
gf: "viewsource",
":": "fillcmdline_notrail",
s: "fillcmdline open search",
S: "fillcmdline tabopen search",
// find mode not suitable for general consumption yet.
// "/": "find",
// "?": "find -1",
// "n": "findnext 1",
// "N": "findnext -1",
M: "gobble 1 quickmark",
B: "fillcmdline bufferall",
b: "fillcmdline buffer",
ZZ: "qall",
f: "hint",
F: "hint -b",
gF: "hint -br",
";i": "hint -i",
";b": "hint -b",
";o": "hint",
";I": "hint -I",
";k": "hint -k",
";y": "hint -y",
";p": "hint -p",
";P": "hint -P",
";r": "hint -r",
";s": "hint -s",
";S": "hint -S",
";a": "hint -a",
";A": "hint -A",
";;": "hint -;",
";#": "hint -#",
";v": "hint -W exclaim_quiet mpv",
";w": "hint -w",
";O": "hint -W fillcmdline_notrail open ",
";W": "hint -W fillcmdline_notrail winopen ",
";T": "hint -W fillcmdline_notrail tabopen ",
";gi": "hint -qi",
";gI": "hint -qI",
";gk": "hint -qk",
";gy": "hint -qy",
";gp": "hint -qp",
";gP": "hint -qP",
";gr": "hint -qr",
";gs": "hint -qs",
";gS": "hint -qS",
";ga": "hint -qa",
";gA": "hint -qA",
";g;": "hint -q;",
";g#": "hint -q#",
";gv": "hint -qW exclaim_quiet mpv",
";gw": "hint -qw",
";gb": "hint -qb",
"<S-Insert>": "mode ignore",
"<CA-Escape>": "mode ignore",
"<CA-`>": "mode ignore",
"<S-Escape>": "mode ignore",
I: "mode ignore",
"<Escape>": "composite mode normal ; hidecmdline",
"<C-[>": "composite mode normal ; hidecmdline",
a: "current_url bmark",
A: "bmark",
zi: "zoom 0.1 true",
zo: "zoom -0.1 true",
zz: "zoom 1",
".": "repeat",
"<SA-ArrowUp><SA-ArrowUp><SA-ArrowDown><SA-ArrowDown><SA-ArrowLeft><SA-ArrowRight><SA-ArrowLeft><SA-ArrowRight>ba":
"open https://www.youtube.com/watch?v=M3iOROuTuMA",
}
/**
* Autocommands that run when certain events happen, and other conditions are met.
*
* Related ex command: `autocmd`.
*/
autocmds = {
/**
* Commands that will be run as soon as Tridactyl loads into a page.
*
* Each key corresponds to a URL fragment which, if contained within the page URL, will run the corresponding command.
*/
DocStart: {
// "addons.mozilla.org": "mode ignore",
},
/**
* Commands that will be run when pages are loaded.
*
* Each key corresponds to a URL fragment which, if contained within the page URL, will run the corresponding command.
*/
DocLoad: {},
/**
* Commands that will be run when pages are unloaded.
*
* Each key corresponds to a URL fragment which, if contained within the page URL, will run the corresponding command.
*/
DocEnd: {
// "emacs.org": "sanitise history",
},
/**
* Commands that will be run when Tridactyl first runs each time you start your browser.
*
* Each key corresponds to a URL fragment which, if contained within the page URL, will run the corresponding command.
*/
TriStart: {
".*": "source_quiet",
},
/**
* Commands that will be run when you enter a tab.
*
* Each key corresponds to a URL fragment which, if contained within the page URL, will run the corresponding command.
*/
TabEnter: {
// "gmail.com": "mode ignore",
},
/**
* Commands that will be run when you leave a tab.
*
* Each key corresponds to a URL fragment which, if contained within the page URL, will run the corresponding command.
*/
TabLeft: {
// Actually, this doesn't work because tabclose closes the current tab
// Too bad :/
// "emacs.org": "tabclose",
},
}
/**
* Map for translating keys directly into other keys in normal-ish modes. For example, if you have an entry in this config option mapping `п` to `g`, then you could type `пп` instead of `gg` or `пi` instead of `gi` or `;п` instead of `;g`. This is primarily useful for international users who don't want to deal with rebuilding their bindings every time tridactyl ships a new default keybind. It's not as good as shipping properly internationalized sets of default bindings, but it's probably as close as we're going to get on a small open-source project like this.
*
* Note that the current implementation does not allow you to "chain" keys, for example, "a"=>"b" and "b"=>"c" for "a"=>"c". You can, however, swap or rotate keys, so "a"=>"b" and "b"=>"a" will work the way you'd expect, as will "a"=>"b" and "b"=>"c" and "c"=>"a".
*/
keytranslatemap = {
// Examples (I think >_>):
// "д": "l", // Russian language
// "é" : "w", // BÉPO
// "h": "j", // Dvorak
// "n": "j", // Colemak
// etc
}
/**
* Whether to use the keytranslatemap in various maps.
*/
keytranslatemodes = {
nmaps: "true",
imaps: "false",
inputmaps: "false",
ignoremaps: "false",
}
/**
* Automatically place these sites in the named container.
*
* Each key corresponds to a URL fragment which, if contained within the page URL, the site will be opened in a container tab instead.
*/
autocontain = o({
//"github.com": "microsoft",
//"youtube.com": "google",
})
/**
* Aliases for the commandline.
*
* You can make a new one with `command alias ex-command`.
*/
exaliases = {
alias: "command",
au: "autocmd",
aucon: "autocontain",
audel: "autocmddelete",
audelete: "autocmddelete",
b: "buffer",
o: "open",
w: "winopen",
t: "tabopen",
tabnew: "tabopen",
tn: "tabnext_gt",
bn: "tabnext_gt",
tnext: "tabnext_gt",
bnext: "tabnext_gt",
tp: "tabprev",
tN: "tabprev",
bp: "tabprev",
bN: "tabprev",
tprev: "tabprev",
bprev: "tabprev",
bfirst: "tabfirst",
blast: "tablast",
tfirst: "tabfirst",
tlast: "tablast",
tab: "buffer",
bd: "tabclose",
bdelete: "tabclose",
quit: "tabclose",
q: "tabclose",
qa: "qall",
sanitize: "sanitise",
tutorial: "tutor",
h: "help",
unmute: "mute unmute",
authors: "credits",
openwith: "hint -W",
"!": "exclaim",
"!s": "exclaim_quiet",
containerremove: "containerdelete",
colourscheme: "set theme",
colours: "colourscheme",
colorscheme: "colourscheme",
colors: "colourscheme",
man: "help",
"!js": "fillcmdline_tmp 3000 !js is deprecated. Please use js instead",
"!jsb":
"fillcmdline_tmp 3000 !jsb is deprecated. Please use jsb instead",
current_url: "composite get_current_url | fillcmdline_notrail ",
}
/**
* Used by `]]` and `[[` to look for links containing these words.
*
* Edit these if you want to add, e.g. other language support.
*/
followpagepatterns = {
next: "^(next|newer)\\b|»|>>|more",
prev: "^(prev(ious)?|older)\\b|«|<<",
}
/**
* The default search engine used by `open search`
*/
searchengine = "google"
/**
* Definitions of search engines for use via `open [keyword]`.
*/
searchurls = {
google: "https://www.google.com/search?q=",
scholar: "https://scholar.google.com/scholar?q=",
googleuk: "https://www.google.co.uk/search?q=",
bing: "https://www.bing.com/search?q=",
duckduckgo: "https://duckduckgo.com/?q=",
yahoo: "https://search.yahoo.com/search?p=",
twitter: "https://twitter.com/search?q=",
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://startpage.com/do/search?language=english&cat=web&query=",
github: "https://github.com/search?utf8=✓&q=",
searx: "https://searx.me/?category_general=on&q=",
cnrtl: "http://www.cnrtl.fr/lexicographie/",
osm: "https://www.openstreetmap.org/search?query=",
mdn: "https://developer.mozilla.org/en-US/search?q=",
gentoo_wiki:
"https://wiki.gentoo.org/index.php?title=Special%3ASearch&profile=default&fulltext=Search&search=",
qwant: "https://www.qwant.com/?q=",
}
/**
* URL the newtab will redirect to.
*
* All usual rules about things you can open with `open` apply, with the caveat that you'll get interesting results if you try to use something that needs `nativeopen`: so don't try `about:newtab`.
*/
newtab = ""
/**
* Whether `:viewsource` will use our own page that you can use Tridactyl binds on, or Firefox's default viewer, which you cannot use Tridactyl on.
*/
viewsource: "tridactyl" | "default" = "tridactyl"
/**
* Which storage to use. Sync storage will synchronise your settings via your Firefox Account.
*/
storageloc: "sync" | "local" = "sync"
/**
* Pages opened with `gH`.
*/
homepages = []
/**
* Characters to use in hint mode.
*
* They are used preferentially from left to right.
*/
hintchars = "hjklasdfgyuiopqwertnmzxcvb"
/**
* The type of hinting to use. `vimperator` will allow you to filter links based on their names by typing non-hint chars. It is recommended that you use this in conjuction with the [[hintchars]] setting, which you should probably set to e.g, `5432167890`.
*/
hintfiltermode: "simple" | "vimperator" | "vimperator-reflow" = "simple"
/**
* Whether to optimise for the shortest possible names for each hint, or to use a simple numerical ordering. If set to `numeric`, overrides `hintchars` setting.
*/
hintnames: "short" | "numeric" = "short"
/**
* Whether to display the names for hints in uppercase.
*/
hintuppercase: "true" | "false" = "true"
/**
* The delay in milliseconds in `vimperator` style hint modes after selecting a hint before you are returned to normal mode.
*
* The point of this is to prevent accidental execution of normal mode binds due to people typing more than is necessary to choose a hint.
*/
hintdelay = "300"
/**
* Controls whether the page can focus elements for you via js
*
* Best used in conjunction with browser.autofocus in `about:config`
*/
allowautofocus: "true" | "false" = "true"
/**
* Whether to use Tridactyl's (bad) smooth scrolling.
*/
smoothscroll: "true" | "false" = "false"
/**
* How viscous you want smooth scrolling to feel.
*/
scrollduration = "100"
/**
* Where to open tabs opened with `tabopen` - to the right of the current tab, or at the end of the tabs.
*/
tabopenpos: "next" | "last" = "next"
/**
* Where to open tabs opened with hinting - as if it had been middle clicked, to the right of the current tab, or at the end of the tabs.
*/
relatedopenpos: "related" | "next" | "last" = "related"
/**
* The name of the voice to use for text-to-speech. You can get the list of installed voices by running the following snippet: `js alert(window.speechSynthesis.getVoices().reduce((a, b) => a + " " + b.name))`
*/
ttsvoice = "default" // chosen from the listvoices list or "default"
/**
* Controls text-to-speech volume. Has to be a number between 0 and 1.
*/
ttsvolume = "1"
/**
* Controls text-to-speech speed. Has to be a number between 0.1 and 10.
*/
ttsrate = "1"
/**
* Controls text-to-speech pitch. Has to be between 0 and 2.
*/
ttspitch = "1"
/**
* If nextinput, <Tab> after gi brings selects the next input
*
* If firefox, <Tab> selects the next selectable element, e.g. a link
*/
gimode: "nextinput" | "firefox" = "nextinput"
/**
* Decides where to place the cursor when selecting non-empty input fields
*/
cursorpos: "beginning" | "end" = "end"
/**
* The theme to use.
*
* Permitted values: run `:composite js tri.styling.THEMES | fillcmdline` to find out.
*/
theme = "default"
/**
* Whether to display the mode indicator or not.
*/
modeindicator: "true" | "false" = "true"
/**
* Milliseconds before registering a scroll in the jumplist
*/
jumpdelay = "3000"
/**
* Default logging levels - 2 === WARNING
*
* NB: these cannot be set directly with `set` - you must use magic words such as `WARNING` or `DEBUG`.
*/
logging = {
messaging: 2,
cmdline: 2,
controller: 2,
containers: 2,
hinting: 2,
state: 2,
excmd: 1,
styling: 2,
}
noiframeon: string[] = []
/**
* Insert / input mode edit-in-$EDITOR command to run
* This has to be a command that stays in the foreground for the whole editing session
* "auto" will attempt to find a sane editor in your path.
* Please send your requests to have your favourite terminal moved further up the list to /dev/null.
* (but we are probably happy to add your terminal to the list if it isn't already there.)
*/
editorcmd = "auto"
/**
* The browser executable to look for in commands such as `restart`. Not as mad as it seems if you have multiple versions of Firefox...
*/
browser = "firefox"
/**
* Which clipboard to store items in. Requires the native messenger to be installed.
*/
yankto: "clipboard" | "selection" | "both" = "clipboard"
/**
* Which clipboard to retrieve items from. Requires the native messenger to be installed.
*
* Permitted values: `clipboard`, or `selection`.
*/
putfrom: "clipboard" | "selection" = "clipboard"
/**
* Clipboard command to try to get the selection from (e.g. `xsel` or `xclip`)
*/
externalclipboardcmd = "auto"
/**
* Set this to something weird if you want to have fun every time Tridactyl tries to update its native messenger.
*/
nativeinstallcmd =
"curl -fsSl https://raw.githubusercontent.com/tridactyl/tridactyl/master/native/install.sh | bash"
/**
* Set this to something weird if you want to have fun every time Tridactyl tries to update its native messenger.
*/
win_nativeinstallcmd = `powershell -NoProfile -InputFormat None -Command "Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/cmcaine/tridactyl/master/native/win_install.ps1'))"`
/**
* Profile directory to use with native messenger with e.g, `guiset`.
*/
profiledir = "auto"
// Container settings
/**
* If enabled, tabopen opens a new tab in the currently active tab's container.
*/
tabopencontaineraware: "true" | "false" = "false"
/**
* If moodeindicator is enabled, containerindicator will color the border of the mode indicator with the container color.
*/
containerindicator: "true" | "false" = "true"
/**
* Autocontain directives create a container if it doesn't exist already.
*/
auconcreatecontainer: "true" | "false" = "true"
/**
* Number of most recent results to ask Firefox for. We display the top 20 or so most frequently visited ones.
*/
historyresults = "50"
/**
* Change this to "clobber" to ruin the "Content Security Policy" of all sites a bit and make Tridactyl run a bit better on some of them, e.g. raw.github*
*/
csp: "untouched" | "clobber" = "untouched"
}
/** @hidden */
const DEFAULTS = o(new default_config())
/** Given an object and a target, extract the target if it exists, else return undefined
@param target path of properties as an array
@hidden
*/
function getDeepProperty(obj, target) {
if (obj !== undefined && target.length) {
return getDeepProperty(obj[target[0]], target.slice(1))
} else {
return obj
}
}
/** Create the key path target if it doesn't exist and set the final property to value.
If the path is an empty array, replace the obj.
@param target path of properties as an array
@hidden
*/
function setDeepProperty(obj, value, target) {
if (target.length > 1) {
// If necessary antecedent objects don't exist, create them.
if (obj[target[0]] === undefined) {
obj[target[0]] = o({})
}
return setDeepProperty(obj[target[0]], value, target.slice(1))
} else {
obj[target[0]] = value
}
}
/** Get the value of the key target.
If the user has not specified a key, use the corresponding key from
defaults, if one exists, else undefined.
@hidden
*/
export function get(...target) {
const user = getDeepProperty(USERCONFIG, target)
const defult = getDeepProperty(DEFAULTS, target)
// Merge results if there's a default value and it's not an Array or primitive.
if (defult && (!Array.isArray(defult) && typeof defult === "object")) {
return Object.assign(o({}), defult, user)
} else {
if (user !== undefined) {
return user
} else {
return defult
}
}
}
/** Get the value of the key target, but wait for config to be loaded from the
database first if it has not been at least once before.
This is useful if you are a content script and you've just been loaded.
@hidden
*/
export async function getAsync(...target) {
if (INITIALISED) {
return get(...target)
} else {
return new Promise(resolve =>
WAITERS.push(() => resolve(get(...target))),
)
}
}
/** Full target specification, then value
e.g.
set("nmaps", "o", "open")
set("search", "default", "google")
set("aucmd", "BufRead", "memrise.com", "open memrise.com")
@hidden
*/
export function set(...args) {
if (args.length < 2) {
throw "You must provide at least two arguments!"
}
const target = args.slice(0, args.length - 1)
const value = args[args.length - 1]
setDeepProperty(USERCONFIG, value, target)
save()
}
/** Delete the key at target if it exists
* @hidden */
export function unset(...target) {
const parent = getDeepProperty(USERCONFIG, target.slice(0, -1))
if (parent !== undefined) delete parent[target[target.length - 1]]
save()
}
/** Save the config back to storage API.
Config is not synchronised between different instances of this module until
sometime after this happens.
@hidden
*/
export async function save(storage: "local" | "sync" = get("storageloc")) {
// let storageobj = storage == "local" ? browser.storage.local : browser.storage.sync
// storageobj.set({CONFIGNAME: USERCONFIG})
let settingsobj = o({})
settingsobj[CONFIGNAME] = USERCONFIG
if (storage == "local") browser.storage.local.set(settingsobj)
else browser.storage.sync.set(settingsobj)
}
/** Updates the config to the latest version.
Proposed semantic for config versionning:
- x.y -> x+1.0 : major architectural changes
- x.y -> x.y+1 : renaming settings/changing their types
There's no need for an updater if you're only adding a new setting/changing
a default setting
When adding updaters, don't forget to set("configversion", newversionnumber)!
@hidden
*/
export async function update() {
let updaters = {
"0.0": async () => {
try {
// Before we had a config system, we had nmaps, and we put them in the
// root namespace because we were young and bold.
let legacy_nmaps = await browser.storage.sync.get("nmaps")
if (Object.keys(legacy_nmaps).length > 0) {
USERCONFIG["nmaps"] = Object.assign(
legacy_nmaps["nmaps"],
USERCONFIG["nmaps"],
)
}
} finally {
set("configversion", "1.0")
}
},
"1.0": () => {
let vimiumgi = getDeepProperty(USERCONFIG, "vimium-gi")
if (vimiumgi === true || vimiumgi === "true")
set("gimode", "nextinput")
else if (vimiumgi === false || vimiumgi === "false")
set("gimode", "firefox")
unset("vimium-gi")
set("configversion", "1.1")
},
}
if (!get("configversion")) set("configversion", "0.0")
const updatetest = v => {
return updaters.hasOwnProperty(v) && updaters[v] instanceof Function
}
while (updatetest(get("configversion"))) {
await updaters[get("configversion")]()
}
}
/** Read all user configuration from storage API then notify any waiting asynchronous calls
asynchronous calls generated by getAsync.
@hidden
*/
async function init() {
let syncConfig = await browser.storage.sync.get(CONFIGNAME)
schlepp(syncConfig[CONFIGNAME])
// Local storage overrides sync
let localConfig = await browser.storage.local.get(CONFIGNAME)
schlepp(localConfig[CONFIGNAME])
await update()
INITIALISED = true
for (let waiter of WAITERS) {
waiter()
}
}
// Listen for changes to the storage and update the USERCONFIG if appropriate.
// TODO: BUG! Sync and local storage are merged at startup, but not by this thing.
browser.storage.onChanged.addListener(async (changes, areaname) => {
if (CONFIGNAME in changes) {
// newValue is undefined when calling browser.storage.AREANAME.clear()
if (changes[CONFIGNAME].newValue !== undefined) {
USERCONFIG = changes[CONFIGNAME].newValue
} else if (areaname === (await get("storageloc"))) {
// If newValue is undefined and AREANAME is the same value as STORAGELOC, the user wants to clean their config
USERCONFIG = o({})
}
}
})
init()

View file

@ -1,29 +0,0 @@
import * as Controller from "./controller_background"
import * as Native from "./native_background"
import Logger from "./logging"
const logger = new Logger("rc")
export async function source(filename = "auto") {
let rctext = ""
if (filename == "auto") {
rctext = await Native.getrc()
} else {
rctext = (await Native.read(filename)).content
}
if (rctext === undefined) return false
runRc(rctext)
return true
}
export async function runRc(rc: string) {
for (let cmd of rcFileToExCmds(rc)) {
await Controller.acceptExCmd(cmd)
}
}
export function rcFileToExCmds(rcText: string): string[] {
const excmds = rcText.split("\n")
// Remove empty and comment lines
return excmds.filter(x => /\S/.test(x) && !x.trim().startsWith('"'))
}

View file

@ -1,63 +1,155 @@
/** Content script entry point */
// We need to grab a lock because sometimes Firefox will decide to insert the content script in the page multiple times
if ((window as any).tridactyl_content_lock !== undefined) {
throw Error("Trying to load Tridactyl, but it's already loaded.")
}
(window as any).tridactyl_content_lock = "locked"
// 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 "./lib/html-tagged-template"
/* import "./commandline_content" */
/* import "./excmds_content" */
/* import "./hinting" */
import * as Logging from "./logging"
import "@src/lib/html-tagged-template"
/* import "@src/content/commandline_content" */
/* import "@src/excmds_content" */
/* import "@src/content/hinting" */
import * as Config from "@src/lib/config"
import * as Logging from "@src/lib/logging"
const logger = new Logging.Logger("content")
logger.debug("Tridactyl content script loaded, boss!")
// Our local state
import { contentState, addContentStateChangedListener } from "./state_content"
import {
contentState,
addContentStateChangedListener,
} from "@src/content/state_content"
// Set up our controller to execute content-mode excmds. All code
// running from this entry point, which is to say, everything in the
// content script, will use the excmds that we give to the module
// here.
import * as controller from "@src/lib/controller"
import * as excmds_content from "@src/.excmds_content.generated"
import { CmdlineCmds } from "@src/content/commandline_cmds"
import { EditorCmds } from "@src/content/editor"
import * as hinting_content from "@src/content/hinting"
controller.setExCmds({
"": excmds_content,
"ex": CmdlineCmds,
"text": EditorCmds,
"hint": hinting_content.getHintCommands()
})
messaging.addListener("excmd_content", messaging.attributeCaller(excmds_content))
messaging.addListener("controller_content", messaging.attributeCaller(controller))
// Hook the keyboard up to the controller
import * as ContentController from "./controller_content"
import { getAllDocumentFrames } from "./dom"
window.addEventListener("keydown", ContentController.acceptKey, true)
document.addEventListener("readystatechange", ev =>
getAllDocumentFrames().map(frame => {
frame.contentWindow.removeEventListener(
"keydown",
ContentController.acceptKey,
true,
)
frame.contentWindow.addEventListener(
"keydown",
ContentController.acceptKey,
true,
)
}),
import * as ContentController from "@src/content/controller_content"
import { getAllDocumentFrames } from "@src/lib/dom"
const guardedAcceptKey = (keyevent: KeyboardEvent) => {
if (!keyevent.isTrusted) return
ContentController.acceptKey(keyevent)
}
function listen(elem) {
elem.removeEventListener("keydown", guardedAcceptKey, true)
elem.removeEventListener(
"keypress",
ContentController.canceller.cancelKeyPress,
true,
)
elem.removeEventListener(
"keyup",
ContentController.canceller.cancelKeyUp,
true,
)
elem.addEventListener("keydown", guardedAcceptKey, true)
elem.addEventListener(
"keypress",
ContentController.canceller.cancelKeyPress,
true,
)
elem.addEventListener(
"keyup",
ContentController.canceller.cancelKeyUp,
true,
)
}
listen(window)
document.addEventListener("readystatechange", _ =>
getAllDocumentFrames().forEach(f => listen(f)),
)
// Prevent pages from automatically focusing elements on load
config.getAsync("preventautofocusjackhammer").then(allowautofocus => {
if (allowautofocus === "false") {
return
}
const preventAutoFocus = () => {
// First, blur whatever element is active. This will make sure
// activeElement is the "default" active element
; (document.activeElement as any).blur()
const elem = document.activeElement as any
// ???: We need to set tabIndex, otherwise we won't get focus/blur events!
elem.tabIndex = 0
const focusElem = () => elem.focus()
elem.addEventListener("blur", focusElem)
elem.addEventListener("focusout", focusElem)
// On top of blur/focusout events, we need to periodically check the
// activeElement is the one we want because blur/focusout events aren't
// always triggered when document.activeElement changes
const interval = setInterval(() => { if (document.activeElement != elem) focusElem() }, 200)
// When the user starts interacting with the page, stop resetting focus
function stopResettingFocus(event: Event) {
if (!event.isTrusted) return
elem.removeEventListener("blur", focusElem)
elem.removeEventListener("focusout", focusElem)
clearInterval(interval)
window.removeEventListener("keydown", stopResettingFocus)
window.removeEventListener("mousedown", stopResettingFocus)
}
window.addEventListener("keydown", stopResettingFocus)
window.addEventListener("mousedown", stopResettingFocus)
}
const tryPreventAutoFocus = () => {
document.removeEventListener("readystatechange", tryPreventAutoFocus)
try {
preventAutoFocus()
} catch (e) {
document.addEventListener("readystatechange", tryPreventAutoFocus)
}
}
tryPreventAutoFocus()
})
// Add various useful modules to the window for debugging
import * as commandline_content from "./commandline_content"
import * as convert from "./convert"
import * as config from "./config"
import * as dom from "./dom"
import * as excmds from "./.excmds_content.generated"
import * as hinting_content from "./hinting"
import * as finding_content from "./finding"
import * as itertools from "./itertools"
import * as messaging from "./messaging"
import state from "./state"
import * as webext from "./lib/webext"
import * as commandline_content from "@src/content/commandline_content"
import * as convert from "@src/lib/convert"
import * as config from "@src/lib/config"
import * as dom from "@src/lib/dom"
import * as excmds from "@src/.excmds_content.generated"
import * as finding_content from "@src/content/finding"
import * as itertools from "@src/lib/itertools"
import * as messaging from "@src/lib/messaging"
import state from "@src/state"
import * as webext from "@src/lib/webext"
import Mark from "mark.js"
import * as keyseq from "./keyseq"
import * as native from "./native_background"
import * as styling from "./styling"
;(window as any).tri = Object.assign(Object.create(null), {
import * as perf from "@src/perf"
import * as keyseq from "@src/lib/keyseq"
import * as native from "@src/lib/native"
import * as styling from "@src/content/styling"
import { EditorCmds as editor } from "@src/content/editor"
import * as updates from "@src/lib/updates"
/* tslint:disable:import-spacing */
; (window as any).tri = Object.assign(Object.create(null), {
browserBg: webext.browserBg,
commandline_content,
convert,
config,
dom,
editor,
excmds,
hinting_content,
finding_content,
hinting_content,
itertools,
logger,
Mark,
@ -68,20 +160,18 @@ import * as styling from "./styling"
l: prom => prom.then(console.log).catch(console.error),
native,
styling,
contentLocation: window.location,
perf,
updates,
})
logger.info("Loaded commandline content?", commandline_content)
// Don't hijack on the newtab page.
if (webext.inContentScript()) {
try {
dom.setupFocusHandler()
dom.hijackPageListenerFunctions()
} catch (e) {
logger.warning("Could not hijack due to CSP:", e)
}
} else {
logger.warning("No export func")
try {
dom.setupFocusHandler()
dom.hijackPageListenerFunctions()
} catch (e) {
logger.warning("Could not hijack due to CSP:", e)
}
if (
@ -89,14 +179,15 @@ if (
window.location.pathname === "/static/newtab.html"
) {
config.getAsync("newtab").then(newtab => {
if (newtab == "about:blank") {
} else if (newtab) {
excmds.open_quiet(newtab)
} else {
document.body.style.height = "100%"
document.body.style.opacity = "1"
document.body.style.overflow = "auto"
document.title = "Tridactyl Top Tips & New Tab Page"
if (newtab !== "about:blank") {
if (newtab) {
excmds.open_quiet(newtab)
} else {
document.body.style.height = "100%"
document.body.style.opacity = "1"
document.body.style.overflow = "auto"
document.title = "Tridactyl Top Tips & New Tab Page"
}
}
})
}
@ -106,12 +197,12 @@ config.getAsync("modeindicator").then(mode => {
if (mode !== "true") return
// Do we want container indicators?
let containerIndicator = config.get("containerindicator")
const containerIndicator = config.get("containerindicator")
// Hide indicator in print mode
// CSS not explicitly added to the dom doesn't make it to print mode:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1448507
let style = document.createElement("style")
const style = document.createElement("style")
style.type = "text/css"
style.innerHTML = `@media print {
.TridactylStatusIndicator {
@ -119,17 +210,22 @@ config.getAsync("modeindicator").then(mode => {
}
}`
let statusIndicator = document.createElement("span")
const statusIndicator = document.createElement("span")
const privateMode = browser.extension.inIncognitoContext
? "TridactylPrivate"
: ""
statusIndicator.className =
"cleanslate TridactylStatusIndicator " + privateMode
"cleanslate TridactylStatusIndicator " +
privateMode +
" TridactylModenormal "
// Dynamically sets the border container color.
if (containerIndicator === "true") {
webext
.activeTabContainer()
.ownTabContainer()
.then(ownTab =>
webext.browserBg.contextualIdentities.get(ownTab.cookieStoreId),
)
.then(container => {
statusIndicator.setAttribute(
"style",
@ -143,10 +239,10 @@ config.getAsync("modeindicator").then(mode => {
// This listener makes the modeindicator disappear when the mouse goes over it
statusIndicator.addEventListener("mouseenter", ev => {
let target = ev.target as any
let rect = target.getBoundingClientRect()
const target = ev.target as any
const rect = target.getBoundingClientRect()
target.classList.add("TridactylInvisible")
let onMouseOut = ev => {
const onMouseOut = ev => {
// If the mouse event happened out of the mode indicator boundaries
if (
ev.clientX < rect.x ||
@ -155,7 +251,7 @@ config.getAsync("modeindicator").then(mode => {
ev.clientY > rect.y + rect.height
) {
target.classList.remove("TridactylInvisible")
window.removeEventListener("mousemouve", onMouseOut)
window.removeEventListener("mousemove", onMouseOut)
}
}
window.addEventListener("mousemove", onMouseOut)
@ -175,12 +271,19 @@ config.getAsync("modeindicator").then(mode => {
})
}
addContentStateChangedListener((property, oldValue, newValue) => {
if (property != "mode") {
return
addContentStateChangedListener((property, oldMode, oldValue, newValue) => {
let mode = newValue
let suffix = ""
let result = ""
if (property !== "mode") {
if (property === "suffix") {
mode = oldMode
suffix = newValue
} else {
return
}
}
let mode = newValue
const privateMode = browser.extension.inIncognitoContext
? "TridactylPrivate"
: ""
@ -201,37 +304,59 @@ config.getAsync("modeindicator").then(mode => {
statusIndicator.textContent = "normal"
// statusIndicator.style.borderColor = "lightgray !important"
} else {
statusIndicator.textContent = mode
result = mode
}
const modeindicatorshowkeys = Config.get("modeindicatorshowkeys")
if (modeindicatorshowkeys === "true" && suffix !== "") {
result = mode + " " + suffix
}
logger.debug(
"statusindicator: ",
result,
";",
"config",
modeindicatorshowkeys,
)
statusIndicator.textContent = result
statusIndicator.className +=
" TridactylMode" + statusIndicator.textContent
if (config.get("modeindicator") !== "true") statusIndicator.remove()
})
})
// Site specific fix for / on GitHub.com
config.getAsync("leavegithubalone").then(v => {
if (v == "true") return
try {
// On quick loading pages, the document is already loaded
// if (document.location.host == "github.com") {
document.body.addEventListener("keydown", function(e) {
if ("/".indexOf(e.key) != -1 && contentState.mode == "normal") {
function protectSlash(e) {
if (!e.isTrusted) return
config.get("blacklistkeys").map(
protkey => {
if (protkey.indexOf(e.key) !== -1 && contentState.mode === "normal") {
e.cancelBubble = true
e.stopImmediatePropagation()
}
})
// }
}
)
}
// Some sites like to prevent firefox's `/` from working so we need to protect
// ourselves against that
// This was originally a github-specific fix
config.getAsync("leavegithubalone").then(v => {
if (v === "true") return
try {
// On quick loading pages, the document is already loaded
document.body.addEventListener("keydown", protectSlash)
} catch (e) {
// But on slower pages we wait for the document to load
window.addEventListener("DOMContentLoaded", () => {
// if (document.location.host == "github.com") {
document.body.addEventListener("keydown", function(e) {
if ("/".indexOf(e.key) != -1 && contentState.mode == "normal") {
e.cancelBubble = true
e.stopImmediatePropagation()
}
})
// }
document.body.addEventListener("keydown", protectSlash)
})
}
})
// Listen for statistics from each content script and send them to the
// background for collection. Attach the observer to the window object
// since there's apparently a bug that causes performance observers to
// be GC'd even if they're still the target of a callback.
; (window as any).tri = Object.assign(window.tri, {
perfObserver: perf.listenForCounters(),
})

View file

@ -0,0 +1,15 @@
import { getCommandlineFns } from "@src/lib/commandline_cmds"
import { messageOwnTab } from "@src/lib/messaging"
const functions = getCommandlineFns({} as any)
type ft = typeof functions
type ArgumentsType<T> = T extends (...args: infer U) => any ? U: never;
export const CmdlineCmds = new Proxy (functions as any, {
get(target, property) {
if (target[property]) {
return (...args) => messageOwnTab("commandline_cmd", property as string, args)
}
return target[property]
}
}) as { [k in keyof ft]: (...args: ArgumentsType<ft[k]>) => Promise<ReturnType<ft[k]>> }

View file

@ -1,8 +1,8 @@
/** Inject an input element into unsuspecting webpages and provide an API for interaction with tridactyl */
import Logger from "./logging"
import * as config from "./config"
import { theme } from "./styling"
import Logger from "@src/lib/logging"
import * as config from "@src/lib/config"
import { theme } from "@src/content/styling"
const logger = new Logger("messaging")
const cmdline_logger = new Logger("cmdline")
@ -17,42 +17,43 @@ const cmdline_logger = new Logger("cmdline")
// inject the commandline iframe into a content page
let cmdline_iframe: HTMLIFrameElement = undefined
const cmdline_iframe = window.document.createElementNS(
"http://www.w3.org/1999/xhtml",
"iframe",
) as HTMLIFrameElement
cmdline_iframe.className = "cleanslate"
cmdline_iframe.setAttribute(
"src",
browser.runtime.getURL("static/commandline.html"),
)
cmdline_iframe.setAttribute("id", "cmdline_iframe")
let enabled = false
/** Initialise the cmdline_iframe element unless the window location is included in a value of config/noiframeon */
/** Initialise the cmdline_iframe element unless the window location is included in a value of config/noiframe */
async function init() {
let noiframeon = await config.getAsync("noiframeon")
enabled =
noiframeon.length == 0 ||
noiframeon.find(url => window.location.href.includes(url)) === undefined
if (enabled && cmdline_iframe === undefined) {
try {
cmdline_iframe = window.document.createElement("iframe")
cmdline_iframe.className = "cleanslate"
cmdline_iframe.setAttribute(
"src",
browser.extension.getURL("static/commandline.html"),
)
cmdline_iframe.setAttribute("id", "cmdline_iframe")
hide()
document.documentElement.appendChild(cmdline_iframe)
// first theming of page root
await theme(window.document.querySelector(":root"))
} catch (e) {
logger.error("Couldn't initialise cmdline_iframe!", e)
}
const noiframe = await config.getAsync("noiframe")
if (noiframe === "false" && !enabled) {
hide()
document.documentElement.appendChild(cmdline_iframe)
enabled = true
// first theming of page root
await theme(window.document.querySelector(":root"))
}
}
// Load the iframe immediately if we can (happens if tridactyl is reloaded or on ImageDocument)
// Else load lazily to avoid upsetting page JS that hates foreign iframes.
try {
init()
} catch (e) {
init().catch(e => {
// Surrender event loop with setTimeout() to page JS in case it's still doing stuff.
document.addEventListener("DOMContentLoaded", () => setTimeout(init, 0))
}
document.addEventListener("DOMContentLoaded", () =>
setTimeout(() => {
init().catch(e =>
logger.error("Couldn't initialise cmdline_iframe!", e),
)
}, 0),
)
})
export function show() {
try {
@ -101,6 +102,11 @@ export function blur() {
}
}
export function hide_and_blur() {
hide()
blur()
}
export function executeWithoutCommandLine(fn) {
let parent
if (cmdline_iframe) {
@ -117,6 +123,6 @@ export function executeWithoutCommandLine(fn) {
return result
}
import * as Messaging from "./messaging"
import * as SELF from "./commandline_content"
import * as Messaging from "@src/lib/messaging"
import * as SELF from "@src/content/commandline_content"
Messaging.addListener("commandline_content", Messaging.attributeCaller(SELF))

View file

@ -0,0 +1,198 @@
import { isTextEditable } from "@src/lib/dom"
import { contentState, ModeName } from "@src/content/state_content"
import Logger from "@src/lib/logging"
import * as controller from "@src/lib/controller"
import { KeyEventLike } from "@src/lib/keyseq"
import * as hinting from "@src/content/hinting"
import * as gobblemode from "@src/parsers/gobblemode"
import * as generic from "@src/parsers/genericmode"
const logger = new Logger("controller")
function PrintableKey(k) {
let result = k.key
if (
result === "Control" ||
result === "Meta" ||
result === "Alt" ||
result === "Shift" ||
result === "OS"
) {
return ""
}
if (k.altKey) {
result = "A-" + result
}
if (k.ctrlKey) {
result = "C-" + result
}
if (k.shiftKey) {
result = "S-" + result
}
if (result.length > 1) {
result = "<" + result + ">"
}
return result
}
/**
* KeyCanceller: keep track of keys that have been cancelled in the keydown
* handler (which takes care of dispatching ex commands) and also cancel them
* in keypress/keyup event handlers. This fixes
* https://github.com/tridactyl/tridactyl/issues/234.
*
* If you make modifications to this class, keep in mind that keyup events
* might not arrive in the same order as the keydown events (e.g. user presses
* A, then B, releases B and then A).
*/
class KeyCanceller {
private keyPress: KeyboardEvent[] = []
private keyUp: KeyboardEvent[] = []
constructor() {
this.cancelKeyUp = this.cancelKeyUp.bind(this)
this.cancelKeyPress = this.cancelKeyPress.bind(this)
}
push(ke: KeyboardEvent) {
this.keyPress.push(ke)
this.keyUp.push(ke)
}
cancelKeyPress(ke: KeyboardEvent) {
if (!ke.isTrusted) return
this.cancelKey(ke, this.keyPress)
}
cancelKeyUp(ke: KeyboardEvent) {
if (!ke.isTrusted) return
this.cancelKey(ke, this.keyUp)
}
private cancelKey(ke: KeyboardEvent, kes: KeyboardEvent[]) {
const index = kes.findIndex(
ke2 =>
ke.altKey === ke2.altKey &&
ke.code === ke2.code &&
ke.composed === ke2.composed &&
ke.ctrlKey === ke2.ctrlKey &&
ke.metaKey === ke2.metaKey &&
ke.shiftKey === ke2.shiftKey &&
ke.target === ke2.target,
)
if ((index >= 0) && (ke instanceof KeyboardEvent)) {
ke.preventDefault()
ke.stopImmediatePropagation()
kes.splice(index, 1)
}
}
}
export const canceller = new KeyCanceller()
/** Accepts keyevents, resolves them to maps, maps to exstrs, executes exstrs */
function* ParserController() {
const parsers: { [mode_name in ModeName]: any } = {
normal: keys => generic.parser("nmaps", keys),
insert: keys => generic.parser("imaps", keys),
input: keys => generic.parser("inputmaps", keys),
ignore: keys => generic.parser("ignoremaps", keys),
hint: hinting.parser,
gobble: gobblemode.parser,
}
while (true) {
let exstr = ""
let keyEvents: KeyEventLike[] = []
try {
while (true) {
const keyevent: KeyEventLike = yield
const shadowRoot =
keyevent instanceof KeyboardEvent
? (keyevent.target as Element).shadowRoot
: null
// _just to be safe_, cache this to make the following
// code more thread-safe.
const currentMode = contentState.mode
const textEditable =
keyevent instanceof KeyboardEvent
? shadowRoot === null
? isTextEditable(keyevent.target as Element)
: isTextEditable(shadowRoot.activeElement)
: false
// This code was sort of the cause of the most serious bug in Tridactyl
// to date (March 2018).
// https://github.com/tridactyl/tridactyl/issues/311
if (
currentMode !== "ignore" &&
currentMode !== "hint" &&
currentMode !== "input"
) {
if (textEditable) {
if (currentMode !== "insert") {
contentState.mode = "insert"
}
} else if (currentMode === "insert") {
contentState.mode = "normal"
}
} else if (currentMode === "input" && !textEditable) {
contentState.mode = "normal"
}
// Accumulate key events. The parser will cut this
// down whenever it's not a valid prefix of a known
// binding, so it can't grow indefinitely unless you
// have a combination of maps that permits bindings of
// unbounded length.
keyEvents.push(keyevent)
const response = (
parsers[contentState.mode] ||
(keys => generic.parser(contentState.mode + "maps", keys))
)(keyEvents)
logger.debug(
currentMode,
contentState.mode,
keyEvents,
response,
)
if ((response.isMatch) && (keyevent instanceof KeyboardEvent)) {
keyevent.preventDefault()
keyevent.stopImmediatePropagation()
canceller.push(keyevent)
}
if (response.exstr) {
exstr = response.exstr
break
} else {
keyEvents = response.keys
// show current keyEvents as a suffix of the contentState
contentState.suffix = keyEvents
.map(x => PrintableKey(x))
.join("")
logger.debug("suffix: ", contentState.suffix)
}
}
controller.acceptExCmd(exstr)
contentState.suffix = ""
} catch (e) {
// Rumsfeldian errors are caught here
logger.error("An error occurred in the content controller: ", e)
}
}
}
export const generator = ParserController() // var rather than let stops weirdness in repl.
generator.next()
/** Feed keys to the ParserController */
export function acceptKey(keyevent: KeyboardEvent) {
return generator.next(keyevent)
}

19
src/content/editor.ts Normal file
View file

@ -0,0 +1,19 @@
import { messageOwnTab, addListener, attributeCaller } from "@src/lib/messaging.ts"
import * as DOM from "@src/lib/dom"
import * as _EditorCmds from "@src/lib/editor.ts"
export const EditorCmds = new Proxy(_EditorCmds, {
get(target, property) {
if (target[property]) {
return (...args) => {
if ((document.activeElement as any).src === browser.runtime.getURL("static/commandline.html")) {
return messageOwnTab("commandline_frame", "editor_function", [property].concat(args))
}
return _EditorCmds[property](DOM.getLastUsedInput(), ...args)
}
}
return target[property]
}
})
addListener("editorfn_content", attributeCaller(EditorCmds))

163
src/content/finding.ts Normal file
View file

@ -0,0 +1,163 @@
import * as config from "@src/lib/config"
import * as DOM from "@src/lib/dom"
import { browserBg, activeTabId } from "@src/lib/webext"
import state from "@src/state"
// The host is the shadow root of a span used to contain all highlighting
// elements. This is the least disruptive way of highlighting text in a page.
// It needs to be placed at the very top of the page.
let host
function getFindHost() {
if (host) {
return host
}
const elem = document.createElement("span")
elem.id = "TridactylFindHost"
elem.className = "cleanslate"
elem.style.position = "absolute"
elem.style.top = "0px"
elem.style.left = "0px"
document.body.appendChild(elem)
host = elem.attachShadow({mode: "closed"})
return host
}
class FindHighlight extends HTMLSpanElement {
public top = Infinity
constructor(private rects, private node) {
super()
; (this as any).unfocus = () => {
for (const node of this.children) {
(node as HTMLElement).style.background = `rgba(127,255,255,0.5)`
}
}
; (this as any).focus = () => {
if (!DOM.isVisible(this.children[0])) {
this.children[0].scrollIntoView({ block: "center", inline: "center" })
}
let parentNode = this.node.parentNode
while (parentNode && !(parentNode instanceof HTMLAnchorElement)) {
parentNode = parentNode.parentNode
}
if (parentNode) {
parentNode.focus()
}
for (const node of this.children) {
(node as HTMLElement).style.background = `rgba(255,127,255,0.5)`
}
}
this.style.position = "absolute"
this.style.top = "0px";
this.style.left = "0px";
for (const rect of rects) {
if (rect.top < this.top) {
this.top = rect.top
}
const highlight = document.createElement("span")
highlight.className = "TridactylFindHighlight"
highlight.style.position = "absolute"
highlight.style.top = `${rect.top}px`
highlight.style.left = `${rect.left}px`
highlight.style.width = `${rect.right - rect.left}px`
highlight.style.height = `${rect.bottom - rect.top}px`
highlight.style.zIndex = "2147483645"
highlight.style.pointerEvents = "none"
this.appendChild(highlight)
}
; (this as any).unfocus()
}
}
customElements.define("find-highlight", FindHighlight, { extends: "span" })
// Highlights corresponding to the last search
let lastHighlights
// Which element of `lastSearch` was last selected
let selected = 0
export async function jumpToMatch(searchQuery, reverse) {
// First, search for the query
const findcase = config.get("findcase")
const sensitive = findcase === "sensitive" || (findcase === "smart" && /[A-Z]/.test(searchQuery))
const findPromise = await browserBg.find.find(searchQuery, {
tabId: await activeTabId(),
caseSensitive: sensitive,
entireWord: false,
includeRangeData: true,
includeRectData: true,
})
state.lastSearchQuery = searchQuery
lastHighlights = []
removeHighlighting()
// We need to grab all text nodes in order to find the corresponding element
const walker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT, null, false)
const nodes = []
let node
do {
node = walker.nextNode()
nodes.push(node)
} while (node)
const results = await findPromise
const host = getFindHost()
let focused = false
for (let i = 0; i < results.count; ++i) {
const data = results.rectData[i]
if (data.rectsAndTexts.rectList.length < 1) {
// When a result does not have any rectangles, it's not visible
continue;
}
const range = results.rangeData[i]
const high = new FindHighlight(data.rectsAndTexts.rectList, nodes[range.startTextNodePos])
host.appendChild(high)
lastHighlights.push(high)
if (!focused && DOM.isVisible(high)) {
focused = true
; (high as any).focus()
selected = lastHighlights.length - 1
}
}
if (lastHighlights.length < 1) {
throw new Error("Pattern not found: " + state.lastSearchQuery)
}
lastHighlights
.sort(reverse ? (a, b) => b.top - a.top : (a, b) => a.top - b.top)
if (!focused) {
selected = 0
/* tslint:disable:no-useless-cast */
; (lastHighlights[selected] as any).focus()
}
}
function drawHighlights(highlights) {
const host = getFindHost()
highlights.forEach(elem => host.appendChild(elem))
}
export function removeHighlighting() {
const host = getFindHost();
while (host.firstChild) host.removeChild(host.firstChild)
}
export function jumpToNextMatch(n: number) {
if (!lastHighlights) {
return state.lastSearchQuery ? jumpToMatch(state.lastSearchQuery, n < 0) : undefined
}
if (!host.firstChild) {
drawHighlights(lastHighlights)
}
if (lastHighlights[selected] === undefined) {
removeHighlighting()
throw new Error("Pattern not found: " + state.lastSearchQuery)
}
/* tslint:disable:no-useless-cast */
; (lastHighlights[selected] as any).unfocus()
selected = (selected + n + lastHighlights.length) % lastHighlights.length
/* tslint:disable:no-useless-cast */
; (lastHighlights[selected] as any).focus()
}

897
src/content/hinting.ts Normal file
View file

@ -0,0 +1,897 @@
/** # Hint mode functions
*
* This file contains functions to interact with hint mode.
*
* If you want to bind them to keyboard shortcuts, be sure to prefix them with "hint.". For example, if you want to bind control-[ to `reset`, use:
*
* ```
* bind --mode=hint <C-[> hint.reset
* ```
*
* Contrary to the main tridactyl help page, this one doesn't tell you whether a specific function is bound to something. For now, you'll have to make do with `:bind` and `:viewconfig`.
*
*/
/** ignore this line */
/** Hint links.
TODO:
important
Connect to input system
Gluing into tridactyl
unimportant
Frames
Redraw on reflow
*/
import * as DOM from "@src/lib/dom"
import { log } from "@src/lib/math"
import {
permutationsWithReplacement,
islice,
izip,
map,
unique,
} from "@src/lib/itertools"
import { contentState } from "@src/content/state_content"
import * as config from "@src/lib/config"
import Logger from "@src/lib/logging"
/** @hidden */
const logger = new Logger("hinting")
import * as keyseq from "@src/lib/keyseq"
/** Calclate the distance between two segments.
* @hidden
* */
function distance(l1: number, r1: number, l2: number, r2: number): number {
if (l1 < r2 && r1 > l2) {
return 0
} else {
return Math.min(Math.abs(l1 - r2), Math.abs(l2 - r1))
}
}
/** Simple container for the state of a single frame's hints.
* @hidden
* */
class HintState {
public focusedHint: Hint
readonly hintHost = document.createElement("div")
readonly hints: Hint[] = []
public selectedHints: Hint[] = []
public filter = ""
public hintchars = ""
constructor(
public filterFunc: HintFilter,
public resolve: (x) => void,
public reject: (x) => void,
public rapid: boolean,
) {
this.hintHost.classList.add("TridactylHintHost", "cleanslate")
}
get activeHints() {
return this.hints.filter(h => !h.flag.hidden)
}
/**
* Remove hinting elements and classes from the DOM
*/
cleanUpHints() {
// Undo any alterations of the hinted elements
for (const hint of this.hints) {
hint.hidden = true
}
// Remove all hints from the DOM.
this.hintHost.remove()
}
resolveHinting() {
this.cleanUpHints()
if (this.rapid) this.resolve(this.selectedHints.map(h => h.result))
else
this.resolve(
this.selectedHints[0] ? this.selectedHints[0].result : "",
)
}
changeFocusedHintIndex(offset) {
const activeHints = this.activeHints
if (!activeHints.length) {
return
}
// Get the index of the currently focused hint
const focusedIndex = activeHints.indexOf(this.focusedHint)
// Unfocus the currently focused hint
this.focusedHint.focused = false
// Focus the next hint, accounting for negative wraparound
const nextFocusedIndex =
(focusedIndex + offset + activeHints.length) % activeHints.length
this.focusedHint = activeHints[nextFocusedIndex]
this.focusedHint.focused = true
}
changeFocusedHintTop() {
const focusedRect = this.focusedHint.rect
// Get all hints from the top area
const topHints = this.activeHints.filter(h => h.rect.top < focusedRect.top && h.rect.bottom < focusedRect.bottom)
if (!topHints.length) {
return
}
// Find the next top hint
const nextFocusedHint = topHints.reduce((a, b) => {
const aDistance = distance(a.rect.left, a.rect.right, focusedRect.left, focusedRect.right)
const bDistance = distance(b.rect.left, b.rect.right, focusedRect.left, focusedRect.right)
if (aDistance < bDistance) {
return a
} else if (aDistance > bDistance) {
return b
} else {
if (a.rect.bottom < b.rect.bottom) {
return b
} else {
return a
}
}
})
// Unfocus the currently focused hint
this.focusedHint.focused = false
// Focus the next hint
this.focusedHint = nextFocusedHint
this.focusedHint.focused = true
}
changeFocusedHintBottom() {
const focusedRect = this.focusedHint.rect
// Get all hints from the bottom area
const bottomHints = this.activeHints.filter(h => h.rect.top > focusedRect.top && h.rect.bottom > focusedRect.bottom)
if (!bottomHints.length) {
return
}
// Find the next bottom hint
const nextFocusedHint = bottomHints.reduce((a, b) => {
const aDistance = distance(a.rect.left, a.rect.right, focusedRect.left, focusedRect.right)
const bDistance = distance(b.rect.left, b.rect.right, focusedRect.left, focusedRect.right)
if (aDistance < bDistance) {
return a
} else if (aDistance > bDistance) {
return b
} else {
if (a.rect.top > b.rect.top) {
return b
} else {
return a
}
}
})
// Unfocus the currently focused hint
this.focusedHint.focused = false
// Focus the next hint
this.focusedHint = nextFocusedHint
this.focusedHint.focused = true
}
changeFocusedHintLeft() {
const focusedRect = this.focusedHint.rect
// Get all hints from the left area
const leftHints = this.activeHints.filter(h => h.rect.left < focusedRect.left && h.rect.right < focusedRect.right)
if (!leftHints.length) {
return
}
// Find the next left hint
const nextFocusedHint = leftHints.reduce((a, b) => {
const aDistance = distance(a.rect.top, a.rect.bottom, focusedRect.top, focusedRect.bottom)
const bDistance = distance(b.rect.top, b.rect.bottom, focusedRect.top, focusedRect.bottom)
if (aDistance < bDistance) {
return a
} else if (aDistance > bDistance) {
return b
} else {
if (a.rect.right < b.rect.right) {
return b
} else {
return a
}
}
})
// Unfocus the currently focused hint
this.focusedHint.focused = false
// Focus the next hint
this.focusedHint = nextFocusedHint
this.focusedHint.focused = true
}
changeFocusedHintRight() {
const focusedRect = this.focusedHint.rect
// Get all hints from the right area
const rightHints = this.activeHints.filter(h => h.rect.left > focusedRect.left && h.rect.right > focusedRect.right)
if (!rightHints.length) {
return
}
// Find the next right hint
const nextFocusedHint = rightHints.reduce((a, b) => {
const aDistance = distance(a.rect.top, a.rect.bottom, focusedRect.top, focusedRect.bottom)
const bDistance = distance(b.rect.top, b.rect.bottom, focusedRect.top, focusedRect.bottom)
if (aDistance < bDistance) {
return a
} else if (aDistance > bDistance) {
return b
} else {
if (a.rect.left > b.rect.left) {
return b
} else {
return a
}
}
})
// Unfocus the currently focused hint
this.focusedHint.focused = false
// Focus the next hint
this.focusedHint = nextFocusedHint
this.focusedHint.focused = true
}
}
/** @hidden*/
let modeState: HintState
/** For each hintable element, add a hint
* @hidden
* */
export function hintPage(
hintableElements: Element[],
onSelect: HintSelectedCallback,
resolve = () => {},
reject = () => {},
rapid = false,
) {
const buildHints: HintBuilder = defaultHintBuilder()
const filterHints: HintFilter = defaultHintFilter()
contentState.mode = "hint"
modeState = new HintState(filterHints, resolve, reject, rapid)
if (!rapid) {
buildHints(hintableElements, hint => {
modeState.cleanUpHints()
hint.result = onSelect(hint.target)
modeState.selectedHints.push(hint)
reset()
})
} else {
buildHints(hintableElements, hint => {
hint.result = onSelect(hint.target)
modeState.selectedHints.push(hint)
})
}
if (! modeState.hints.length) {
// No more hints to display
reset()
return
}
// There are multiple hints. Normally we would just show all of them, but
// we try to be clever here. Automatically select the first one if all the
// conditions are true:
// - it is <a>
// - its href is not empty (does not point to the page itself)
// - its href is not javascript
// - all the remaining hints
// - are either _not_ <a>
// - or their href points to the sampe place as first one
const firstTarget = modeState.hints[0].target
const firstTargetIsSelectable = (): boolean => {
return firstTarget instanceof HTMLAnchorElement &&
firstTarget.href !== "" &&
!firstTarget.href.startsWith("javascript:")
}
const allTargetsAreEqual = (): boolean => {
return undefined === modeState.hints.find(h => {
return (
!(h.target instanceof HTMLAnchorElement) ||
h.target.href !== (firstTarget as HTMLAnchorElement).href
)
})
}
if (modeState.hints.length == 1 ||
(firstTargetIsSelectable() && allTargetsAreEqual())) {
// There is just a single link or all the links point to the same
// place. Select it.
modeState.cleanUpHints()
modeState.hints[0].select()
reset()
return
}
// Just focus first link
modeState.focusedHint = modeState.hints[0]
modeState.focusedHint.focused = true
document.documentElement.appendChild(modeState.hintHost)
}
/** @hidden */
function defaultHintBuilder() {
switch (config.get("hintfiltermode")) {
case "simple":
return buildHintsSimple
case "vimperator":
return buildHintsVimperator
case "vimperator-reflow":
return buildHintsVimperator
}
}
/** @hidden */
function defaultHintFilter() {
switch (config.get("hintfiltermode")) {
case "simple":
return filterHintsSimple
case "vimperator":
return filterHintsVimperator
case "vimperator-reflow":
return fstr => filterHintsVimperator(fstr, true)
}
}
/** @hidden */
function defaultHintChars() {
if (config.get("hintnames") === "numeric") {
return "1234567890"
}
return config.get("hintchars")
}
/** An infinite stream of hints
@hidden
Earlier hints prefix later hints
*/
function* hintnames_simple(
hintchars = defaultHintChars(),
): IterableIterator<string> {
for (let taglen = 1; true; taglen++) {
yield* map(permutationsWithReplacement(hintchars, taglen), e =>
e.join(""),
)
}
}
/** Shorter hints
Hints that are prefixes of other hints are a bit annoying because you have to select them with Enter or Space.
This function removes hints that prefix other hints by observing that:
let h = hintchars.length
if n < h ** 2
then n / h = number of single character hintnames that would prefix later hints
So it removes them. This function is not yet clever enough to realise that if n > h ** 2 it should remove
h + (n - h**2 - h) / h ** 2
and so on, but we hardly ever see that many hints, so whatever.
@hidden
*/
function* hintnames_short(
n: number,
hintchars = defaultHintChars(),
): IterableIterator<string> {
const source = hintnames_simple(hintchars)
const num2skip = Math.floor(n / hintchars.length)
yield* islice(source, num2skip, n + num2skip)
}
/** Uniform length hintnames
* @hidden
* */
function* hintnames_uniform(
n: number,
hintchars = defaultHintChars(),
): IterableIterator<string> {
if (n <= hintchars.length) yield* islice(hintchars[Symbol.iterator](), n)
else {
// else calculate required length of each tag
const taglen = Math.ceil(log(n, hintchars.length))
// And return first n permutations
yield* map(
islice(permutationsWithReplacement(hintchars, taglen), n),
perm => {
return perm.join("")
},
)
}
}
/** @hidden */
function* hintnames_numeric(n: number): IterableIterator<string> {
for (let i = 1; i <= n; i++) {
yield String(i)
}
}
/** @hidden */
function* hintnames(
n: number,
hintchars = defaultHintChars(),
): IterableIterator<string> {
switch (config.get("hintnames")) {
case "numeric":
yield* hintnames_numeric(n)
case "uniform":
yield* hintnames_uniform(n, hintchars)
default:
yield* hintnames_short(n, hintchars)
}
}
/** @hidden */
type HintSelectedCallback = (x: any) => any
/** Place a flag by each hintworthy element
@hidden */
class Hint {
public readonly flag = document.createElement("span")
public readonly rect: ClientRect = null
public result: any = null
constructor(
public readonly target: Element,
public readonly name: string,
public readonly filterData: any,
private readonly onSelect: HintSelectedCallback,
) {
// We need to compute the offset for elements that are in an iframe
let offsetTop = 0
let offsetLeft = 0
if (target.ownerDocument !== document) {
const iframe = DOM.getAllDocumentFrames().find(
frame => frame.contentDocument === target.ownerDocument,
)
const rect = iframe.getClientRects()[0]
offsetTop += rect.top
offsetLeft += rect.left
}
const rect = target.getClientRects()[0]
this.rect = {
top: rect.top + offsetTop,
bottom: rect.bottom + offsetTop,
left: rect.left + offsetLeft,
right: rect.right + offsetLeft,
width: rect.width,
height: rect.height
}
this.flag.textContent = name
this.flag.className = "TridactylHint"
if (config.get("hintuppercase") === "true") {
this.flag.classList.add("TridactylHintUppercase")
}
this.flag.classList.add("TridactylHint" + target.tagName)
this.flag.style.cssText = `
top: ${window.scrollY + this.rect.top}px !important;
left: ${window.scrollX + this.rect.left}px !important;
`
modeState.hintHost.appendChild(this.flag)
this.hidden = false
}
// These styles would be better with pseudo selectors. Can we do custom ones?
// If not, do a state machine.
set hidden(hide: boolean) {
this.flag.hidden = hide
if (hide) {
this.focused = false
this.target.classList.remove("TridactylHintElem")
} else {
this.target.classList.add("TridactylHintElem")
}
}
set focused(focus: boolean) {
if (focus) {
this.target.classList.add("TridactylHintActive")
this.target.classList.remove("TridactylHintElem")
} else {
this.target.classList.add("TridactylHintElem")
this.target.classList.remove("TridactylHintActive")
}
}
select() {
this.onSelect(this)
}
}
/** @hidden */
type HintBuilder = (els: Element[], onSelect: HintSelectedCallback) => void
/** @hidden */
function buildHintsSimple(els: Element[], onSelect: HintSelectedCallback) {
const names = hintnames(els.length)
for (const [el, name] of izip(els, names)) {
logger.debug({ el, name })
modeState.hintchars += name
modeState.hints.push(new Hint(el, name, null, onSelect))
}
}
/** @hidden */
function buildHintsVimperator(els: Element[], onSelect: HintSelectedCallback) {
const names = hintnames(els.length)
// escape the hintchars string so that strange things don't happen
// when special characters are used as hintchars (for example, ']')
const escapedHintChars = defaultHintChars().replace(/^\^|[-\\\]]/g, "\\$&")
const filterableTextFilter = new RegExp("[" + escapedHintChars + "]", "g")
for (const [el, name] of izip(els, names)) {
let ft = elementFilterableText(el)
// strip out hintchars
ft = ft.replace(filterableTextFilter, "")
logger.debug({ el, name, ft })
modeState.hintchars += name + ft
modeState.hints.push(new Hint(el, name, ft, onSelect))
}
}
/** @hidden */
function elementFilterableText(el: Element): string {
const nodename = el.nodeName.toLowerCase()
let text: string
if (nodename === "input") {
text = (el as HTMLInputElement).value
} else if (0 < el.textContent.length) {
text = el.textContent
} else if (el.hasAttribute("title")) {
text = el.getAttribute("title")
} else {
text = el.innerHTML
}
// Truncate very long text values
return text.slice(0, 2048).toLowerCase() || ""
}
/** @hidden */
type HintFilter = (s: string) => void
/** Show only hints prefixed by fstr. Focus first match
@hidden */
function filterHintsSimple(fstr) {
const active: Hint[] = []
let foundMatch
for (const h of modeState.hints) {
if (!h.name.startsWith(fstr)) h.hidden = true
else {
if (!foundMatch) {
h.focused = true
modeState.focusedHint = h
foundMatch = true
}
h.hidden = false
active.push(h)
}
}
if (active.length === 1) {
selectFocusedHint()
}
}
/** Partition the filter string into hintchars and content filter strings.
Apply each part in sequence, reducing the list of active hints.
Update display after all filtering, adjusting labels if appropriate.
Consider: This is a poster child for separating data and display. If they
weren't so tied here we could do a neat dynamic programming thing and just
throw the data at a reactalike.
@hidden
*/
function filterHintsVimperator(fstr, reflow = false) {
/** Partition a fstr into a tagged array of substrings */
function partitionFstr(fstr): Array<{ str: string; isHintChar: boolean }> {
const peek = a => a[a.length - 1]
const hintChars = defaultHintChars()
// For each char, either add it to the existing run if there is one and
// it's a matching type or start a new run
const runs = []
for (const char of fstr) {
const isHintChar = hintChars.includes(char)
if (!peek(runs) || peek(runs).isHintChar !== isHintChar) {
runs.push({ str: char, isHintChar })
} else {
peek(runs).str += char
}
}
return runs
}
function rename(hints) {
const names = hintnames(hints.length)
for (const [hint, name] of izip(hints, names)) {
hint.name = name
}
}
// Start with all hints
let active = modeState.hints
// If we're reflowing, the names may be wrong at this point, so apply the original names.
if (reflow) rename(active)
// Filter down (renaming as required)
for (const run of partitionFstr(fstr)) {
if (run.isHintChar) {
// Filter by label
active = active.filter(hint => hint.name.startsWith(run.str))
} else {
// By text
active = active.filter(hint => hint.filterData.includes(run.str))
if (reflow) rename(active)
}
}
// Update display
// Unfocus the focused hint - must be before hiding the hint
if (modeState.focusedHint) {
modeState.focusedHint.focused = false
modeState.focusedHint = undefined
}
// Set hidden state of the hints
for (const hint of modeState.hints) {
if (active.includes(hint)) {
hint.hidden = false
hint.flag.textContent = hint.name
} else {
hint.hidden = true
}
}
// Focus first hint
if (active.length) {
modeState.focusedHint = active[0]
modeState.focusedHint.focused = true
}
// Select focused hint if it's the only match
if (active.length === 1) {
selectFocusedHint(true)
}
}
/**
* Remove all hints, reset STATE.
**/
function reset() {
if (modeState) {
modeState.cleanUpHints()
modeState.resolveHinting()
}
modeState = undefined
contentState.mode = "normal"
}
function popKey() {
modeState.filter = modeState.filter.slice(0, -1)
modeState.filterFunc(modeState.filter)
}
/** Add key to filtstr and filter */
function pushKey(key) {
// The new key can be used to filter the hints
const originalFilter = modeState.filter
modeState.filter += key
modeState.filterFunc(modeState.filter)
if (modeState && !modeState.activeHints.length) {
// There are no more active hints, undo the change to the filter
modeState.filter = originalFilter
modeState.filterFunc(modeState.filter)
}
}
/** Just run pushKey(" "). This is needed because ex commands ignore whitespace. */
function pushSpace() {
return pushKey(" ")
}
/** Array of hintable elements in viewport
Elements are hintable if
1. they can be meaningfully selected, clicked, etc
2. they're visible
1. Within viewport
2. Not hidden by another element
@hidden
*/
export function hintables(selectors = DOM.HINTTAGS_selectors, withjs = false) {
let elems = DOM.getElemsBySelector(selectors, [])
if (withjs) {
elems = elems.concat(DOM.hintworthy_js_elems)
elems = unique(elems)
}
return elems.filter(DOM.isVisible)
}
/** Returns elements that point to a saveable resource
* @hidden
*/
export function saveableElements() {
return DOM.getElemsBySelector(DOM.HINTTAGS_saveable, [DOM.isVisible])
}
/** Get array of images in the viewport
* @hidden
*/
export function hintableImages() {
return DOM.getElemsBySelector(DOM.HINTTAGS_img_selectors, [DOM.isVisible])
}
/** Get array of selectable elements that display a text matching either plain
* text or RegExp rule
* @hidden
*/
export function hintByText(match: string|RegExp) {
return DOM.getElemsBySelector(DOM.HINTTAGS_filter_by_text_selectors, [
DOM.isVisible,
hint => {
let text
if (hint instanceof HTMLInputElement) {
// tslint:disable-next-line:no-useless-cast
text = (hint as HTMLInputElement).value
} else {
text = hint.textContent
}
if (match instanceof RegExp) {
return text.match(match) !== null
} else {
return text.toUpperCase().includes(match.toUpperCase())
}
}
])
}
/** Array of items that can be killed with hint kill
@hidden
*/
export function killables() {
return DOM.getElemsBySelector(DOM.HINTTAGS_killable_selectors, [
DOM.isVisible,
])
}
/** HintPage wrapper, accepts CSS selectors to build a list of elements
* @hidden
* */
export function pipe(
selectors = DOM.HINTTAGS_selectors,
action: HintSelectedCallback = _ => _,
rapid = false,
jshints = true,
): Promise<[Element, number]> {
return new Promise((resolve, reject) => {
hintPage(hintables(selectors, jshints), action, resolve, reject, rapid)
})
}
/** HintPage wrapper, accepts array of elements to hint
* @hidden
* */
export function pipe_elements(
elements: any = DOM.elementsWithText,
action: HintSelectedCallback = _ => _,
rapid = false,
): Promise<[Element, number]> {
return new Promise((resolve, reject) => {
hintPage(elements, action, resolve, reject, rapid)
})
}
function selectFocusedHint(delay = false) {
logger.debug("Selecting hint.", contentState.mode)
const focused = modeState.focusedHint
const selectFocusedHintInternal = () => {
modeState.filter = ""
modeState.hints.forEach(h => (h.hidden = false))
focused.select()
}
if (delay) setTimeout(selectFocusedHintInternal, config.get("hintdelay"))
else selectFocusedHintInternal()
}
function focusNextHint() {
logger.debug("Focusing next hint")
modeState.changeFocusedHintIndex(1)
}
function focusPreviousHint() {
logger.debug("Focusing previous hint")
modeState.changeFocusedHintIndex(-1)
}
function focusTopHint() {
logger.debug("Focusing top hint")
modeState.changeFocusedHintTop()
}
function focusBottomHint() {
logger.debug("Focusing bottom hint")
modeState.changeFocusedHintBottom()
}
function focusLeftHint() {
logger.debug("Focusing left hint")
modeState.changeFocusedHintLeft()
}
function focusRightHint() {
logger.debug("Focusing right hint")
modeState.changeFocusedHintRight()
}
/** @hidden */
export function parser(keys: KeyboardEvent[]) {
const parsed = keyseq.parse(keys,
keyseq.mapstrMapToKeyMap(new Map((Object.entries(config.get("hintmaps")) as any)
.filter(([key, value]) => value != ""))))
if (parsed.isMatch === true) {
return parsed
}
// Ignore modifiers since they can't match text
const simplekeys = keys.filter(key => !keyseq.hasModifiers(key))
let exstr
if (simplekeys.length > 1) {
exstr = simplekeys.reduce((acc, key) => `hint.pushKey ${key.key};`, "composite ")
} else if (simplekeys.length === 1) {
exstr = `hint.pushKey ${simplekeys[0].key}`
} else {
return { keys: [], isMatch: false }
}
return { exstr, value: exstr, isMatch: true }
}
/** @hidden*/
export function getHintCommands() {
return {
reset,
focusPreviousHint,
focusNextHint,
focusTopHint,
focusBottomHint,
focusLeftHint,
focusRightHint,
selectFocusedHint,
pushKey,
pushSpace,
popKey,
};
};

View file

@ -1,14 +1,8 @@
import * as Native from "./native_background"
import * as config from "./config"
import * as config from "@src/lib/config"
type scrollingDirection = "scrollLeft" | "scrollTop"
// Stores elements that are currently being horizontally scrolled
let horizontallyScrolling = new Map()
// Stores elements that are currently being vertically scrolled
let verticallyScrolling = new Map()
let opts = { smooth: null, duration: null }
const opts = { smooth: null, duration: null }
async function getSmooth() {
if (opts.smooth === null)
opts.smooth = await config.getAsync("smoothscroll")
@ -17,16 +11,11 @@ async function getSmooth() {
async function getDuration() {
if (opts.duration === null)
opts.duration = await config.getAsync("scrollduration")
return Number.parseInt(opts.duration)
return opts.duration
}
browser.storage.onChanged.addListener(changes => {
if ("userconfig" in changes) {
if ("smoothscroll" in changes.userconfig.newValue)
opts.smooth = changes.userconfig.newValue["smoothscroll"]
if ("scrollduration" in changes.userconfig.newValue)
opts.duration = Number.parseInt(changes.userconfig.newValue["scrollduration"])
}
})
config.addChangeListener("smoothscroll", (prev, cur) => (opts.smooth = cur))
config.addChangeListener("scrollduration", (prev, cur) => (opts.duration = cur))
class ScrollingData {
// time at which the scrolling animation started
@ -45,7 +34,7 @@ class ScrollingData {
* pos: "scrollLeft" if the element should be scrolled on the horizontal axis, "scrollTop" otherwise
*/
constructor(
private elem: HTMLElement,
private elem: Node,
private pos: scrollingDirection = "scrollTop",
) {}
@ -59,22 +48,32 @@ class ScrollingData {
if (this.startTime === undefined) {
this.startTime = performance.now()
}
let elapsed = performance.now() - this.startTime
const elapsed = performance.now() - this.startTime
// If the animation should be done, return the position the element should have
if (elapsed >= this.duration || this.elem[this.pos] == this.endPos)
if (elapsed >= this.duration || this.elem[this.pos] === this.endPos)
return this.endPos
let result = (this.endPos - this.startPos) * elapsed / this.duration
if (result >= 1 || result <= -1) return this.startPos + result
return this.elem[this.pos] + (this.startPos < this.endPos ? 1 : -1)
let result = this.startPos + (((this.endPos - this.startPos) * elapsed) / this.duration)
if (this.startPos < this.endPos) {
// We need to ceil() because only highdpi screens have a decimal this.elem[this.pos]
result = Math.ceil(result)
// We *have* to make progress, otherwise we'll think the element can't be scrolled
if (result == this.elem[this.pos])
result += 1
} else {
result = Math.floor(result)
if (result == this.elem[this.pos])
result -= 1
}
return result
}
/** Updates the position of this.elem */
/** Updates the position of this.elem, returns true if the element has been scrolled, false otherwise. */
scrollStep() {
let val = this.elem[this.pos]
const val = this.elem[this.pos]
this.elem[this.pos] = this.getStep()
return val != this.elem[this.pos]
return val !== this.elem[this.pos]
}
/** Calls this.scrollStep() until the element has been completely scrolled
@ -82,22 +81,20 @@ class ScrollingData {
scheduleStep() {
// If scrollStep() scrolled the element, reschedule a step
// Otherwise, register that the element stopped scrolling
window.requestAnimationFrame(
() =>
this.scrollStep()
? this.scheduleStep()
: (this.scrolling = false),
window.requestAnimationFrame(() =>
this.scrollStep() ? this.scheduleStep() : (this.scrolling = false),
)
}
scroll(distance: number, duration: number) {
this.startTime = performance.now()
this.startPos = this.elem[this.pos]
this.endPos = this.elem[this.pos] + distance
this.endPos = this.startPos + distance
this.duration = duration
// If we're already scrolling we don't need to try to scroll
if (this.scrolling) return true
;(this.elem.style as any).scrollBehavior = "unset"
if ("style" in this.elem)
(this.elem as any).style.scrollBehavior = "unset"
this.scrolling = this.scrollStep()
if (this.scrolling)
// If the element can be scrolled, scroll until animation completion
@ -106,32 +103,41 @@ class ScrollingData {
}
}
// Stores elements that are currently being horizontally scrolled
const horizontallyScrolling = new Map<Node, ScrollingData>()
// Stores elements that are currently being vertically scrolled
const verticallyScrolling = new Map<Node, ScrollingData>()
/** Tries to scroll e by x and y pixel, make the smooth scrolling animation
* last duration milliseconds
*/
export async function scroll(
x: number,
y: number,
e: HTMLElement,
duration: number = undefined,
x: number = 0,
y: number = 0,
e: Node,
duration?: number,
) {
let smooth = await getSmooth()
if (smooth == "false") duration = 0
const smooth = await getSmooth()
if (smooth === "false") duration = 0
else if (duration === undefined) duration = await getDuration()
let result = false
if (x != 0) {
if (x !== 0) {
// Don't create a new ScrollingData object if the element is already
// being scrolled
let scrollData = horizontallyScrolling.get(e)
if (!scrollData) scrollData = new ScrollingData(e, "scrollLeft")
horizontallyScrolling.set(e, scrollData)
if (!scrollData) {
scrollData = new ScrollingData(e, "scrollLeft")
horizontallyScrolling.set(e, scrollData)
}
result = result || scrollData.scroll(x, duration)
}
if (y != 0) {
if (y !== 0) {
let scrollData = verticallyScrolling.get(e)
if (!scrollData) scrollData = new ScrollingData(e, "scrollTop")
verticallyScrolling.set(e, scrollData)
if (!scrollData) {
scrollData = new ScrollingData(e, "scrollTop")
verticallyScrolling.set(e, scrollData)
}
result = result || scrollData.scroll(y, duration)
}
return result
@ -149,50 +155,58 @@ let lastY = 0
export async function recursiveScroll(
x: number,
y: number,
nodes: Element[] = undefined,
ignore: Element[] = [],
node?: Element,
stopAt?: Element,
) {
let startingFromCached = false
if (!nodes) {
// Check if x and lastX have the same sign and if y and lastY have the same sign
if (lastRecursiveScrolled && (x ^ lastX) >= 0 && (y ^ lastY) >= 0) {
if (!node) {
const sameSignX = x < 0 === lastX < 0
const sameSignY = y < 0 === lastY < 0
if (lastRecursiveScrolled && sameSignX && sameSignY) {
// We're scrolling in the same direction as the previous time so
// let's try to pick up from where we left
startingFromCached = true
nodes = [lastRecursiveScrolled]
node = lastRecursiveScrolled
} else {
nodes = [document.documentElement]
node = document.documentElement
}
}
let index = 0
let now = performance.now()
let treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT)
do {
let node
do {
node = nodes[index++] as any
} while (ignore.includes(node))
// If node is undefined or if we managed to scroll it
if (!node || (await scroll(x, y, node))) {
if (
(await scroll(x, y, treeWalker.currentNode)) ||
((treeWalker.currentNode as any).contentDocument &&
!(treeWalker.currentNode as any).src.startsWith("moz-extension://") &&
(await recursiveScroll(
x,
y,
(treeWalker.currentNode as any).contentDocument.body,
)))
) {
// Cache the node for next time and stop trying to scroll
lastRecursiveScrolled = node
return
lastRecursiveScrolled = treeWalker.currentNode
lastX = x
lastY = y
return true
}
// Otherwise, add its children to the nodes that could be scrolled
nodes = nodes.concat(Array.from(node.children))
if (node.contentDocument) nodes.push(node.contentDocument.body)
} while (index < nodes.length)
// If we reached this part, this means that we couldn't find an element to scroll
// If we started from a cached element, we can try to start again from the
// top of the document and ignore the cached element this time.
// It might be possible to further improve performance by first trying to
// recursiveScroll lastRecursiveScrolled sibling elements and only if
// that fails its parents but this seems unnecessary for now
} while (treeWalker.nextNode())
// If we started from a cached node, we could try the nodes before it
if (startingFromCached) {
recursiveScroll(
x,
y,
[document.documentElement],
[lastRecursiveScrolled],
)
treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT)
do {
// If node is undefined or if we managed to scroll it
if (await scroll(x, y, treeWalker.currentNode)) {
// Cache the node for next time and stop trying to scroll
lastRecursiveScrolled = treeWalker.currentNode
lastX = x
lastY = y
return true
}
} while (treeWalker.previousNode())
}
lastRecursiveScrolled = null
lastX = x
lastY = y
return false
}

View file

@ -1,4 +1,4 @@
import Logger from "./logging"
import Logger from "@src/lib/logging"
const logger = new Logger("state")
export type ModeName =
@ -8,7 +8,6 @@ export type ModeName =
| "ignore"
| "gobble"
| "input"
| "find"
export class PrevInput {
inputId: string
@ -26,14 +25,16 @@ class State {
jumppos: undefined,
},
]
suffix: string = ""
}
export type ContentStateProperty = "mode" | "cmdHistory" | "prevInputs"
export type ContentStateProperty = "mode" | "cmdHistory" | "prevInputs" | "suffix"
export type ContentStateChangedCallback = (
property: ContentStateProperty,
oldValue: any,
newValue: any,
suffix: any,
) => void
const onChangedListeners: ContentStateChangedCallback[] = []
@ -47,18 +48,19 @@ export function addContentStateChangedListener(
export const contentState = (new Proxy(
{ mode: "normal" },
{
get: function(target, property: ContentStateProperty) {
get(target, property: ContentStateProperty) {
return target[property]
},
set: function(target, property: ContentStateProperty, newValue) {
set(target, property: ContentStateProperty, newValue) {
logger.debug("Content state changed!", property, newValue)
const oldValue = target[property]
const mode = target.mode
target[property] = newValue
for (let listener of onChangedListeners) {
listener(property, oldValue, newValue)
for (const listener of onChangedListeners) {
listener(property, mode, oldValue, newValue)
}
return true
},

View file

@ -1,6 +1,7 @@
import { staticThemes } from "./.metadata.generated"
import * as config from "./config"
import * as Logging from "./logging"
import { staticThemes } from "@src/.metadata.generated"
import * as config from "@src/lib/config"
import * as Logging from "@src/lib/logging"
import { browserBg, ownTabId } from "@src/lib/webext"
const logger = new Logging.Logger("styling")
@ -18,19 +19,43 @@ function prefixTheme(name) {
// At the moment elements are only ever `:root` and so this array and stuff is all a bit overdesigned.
const THEMED_ELEMENTS = []
let insertedCSS = false
const customCss = {
allFrames: true,
matchAboutBlank: true,
code: "",
}
export async function theme(element) {
// Remove any old theme
for (let theme of THEMES.map(prefixTheme)) {
for (const theme of THEMES.map(prefixTheme)) {
element.classList.remove(theme)
}
if (insertedCSS) {
// Typescript doesn't seem to be aware than remove/insertCSS's tabid
// argument is optional
await browserBg.tabs.removeCSS(await ownTabId(), customCss)
insertedCSS = false
}
let newTheme = await config.getAsync("theme")
const newTheme = await config.getAsync("theme")
// Add a class corresponding to config.get('theme')
if (newTheme !== "default") {
element.classList.add(prefixTheme(newTheme))
}
// Insert custom css if needed
if (newTheme !== "default" && !THEMES.includes(newTheme)) {
customCss.code = await config.getAsync("customthemes", newTheme)
if (customCss.code) {
await browserBg.tabs.insertCSS(await ownTabId(), customCss)
insertedCSS = true
} else {
logger.error("Theme " + newTheme + " couldn't be found.")
}
}
// Record for re-theming
// considering only elements :root (page and cmdline_iframe)
// TODO:
@ -45,29 +70,22 @@ export async function theme(element) {
function retheme() {
THEMED_ELEMENTS.forEach(element => {
try {
theme(element)
} catch (e) {
theme(element).catch(e => {
logger.warning(
`Failed to retheme element "${element}". Error: ${e}`,
)
}
})
})
}
// Hacky listener
browser.storage.onChanged.addListener((changes, areaname) => {
if ("userconfig" in changes) {
retheme()
}
})
config.addChangeListener("theme", retheme)
// Sometimes pages will overwrite class names of elements. We use a MutationObserver to make sure that the HTML element always has a TridactylTheme class
// We can't just call theme() because it would first try to remove class names from the element, which would trigger the MutationObserver before we had a chance to add the theme class and thus cause infinite recursion
let cb = async mutationList => {
let theme = await config.getAsync("theme")
const cb = async mutationList => {
const theme = await config.getAsync("theme")
mutationList
.filter(m => m.target.className.search(prefixTheme("")) == -1)
.filter(m => m.target.className.search(prefixTheme("")) === -1)
.forEach(m => m.target.classList.add(prefixTheme(theme)))
}

83
src/content/toys.ts Normal file
View file

@ -0,0 +1,83 @@
/**
* Copyright (c) 2018 by Ebram Marzouk (https://codepen.io/P3R0/pen/MwgoKv)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export function jack_in() {
// chinese characters - taken from the unicode charset
const chinese = "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑".split(
"",
)
const colour = "#0F0" // green text
rain(chinese, colour)
}
export function no_mouse() {
rain([" "], "#FFF", 0) // No characters, unused colour code, no darkening
}
export let snow = () => rain(["❄"], "#FFF", 0.15)
export function rain(characters: string[], colour, darkening = 0.05) {
const d = document.createElement("div")
d.style.position = "fixed"
d.style.display = "block"
d.style.width = "100%"
d.style.height = "100%"
d.style.top = "0"
d.style.left = "0"
d.style.right = "0"
d.style.bottom = "0"
d.style.zIndex = "1000"
d.style.opacity = "0.5"
const c = document.createElement("canvas")
d.appendChild(c)
document.body.appendChild(d)
const ctx = c.getContext("2d")
// making the canvas full screen
c.height = window.innerHeight
c.width = window.innerWidth
// converting the string into an array of single characters
const font_size = 10
const columns = c.width / font_size // number of columns for the rain
// an array of drops - one per column
const drops = []
// x below is the x coordinate
// 1 = y co-ordinate of the drop(same for every drop initially)
for (let x = 0; x < columns; x++) drops[x] = 1
// drawing the characters
function draw() {
// Black BG for the canvas
// translucent BG to show trail
ctx.fillStyle = "rgba(0, 0, 0, " + darkening + ")"
ctx.fillRect(0, 0, c.width, c.height)
ctx.fillStyle = colour
ctx.font = font_size + "px arial"
// looping over drops
for (let i = 0; i < drops.length; i++) {
// a random chinese character to print
const text = characters[Math.floor(Math.random() * characters.length)]
// x = i*font_size, y = value of drops[i]*font_size
ctx.fillText(text, i * font_size, drops[i] * font_size)
// sending the drop back to the top randomly after it has crossed the screen
// adding a randomness to the reset to make the drops scattered on the Y axis
if (drops[i] * font_size > c.height && Math.random() > 0.975)
drops[i] = 0
// incrementing Y coordinate
drops[i]++
}
}
setInterval(draw, 33)
}

View file

@ -1,38 +0,0 @@
import { parser as exmode_parser } from "./parsers/exmode"
import { repeat } from "./.excmds_background.generated"
import Logger from "./logging"
const logger = new Logger("controller")
export let last_ex_str = ""
/** Parse and execute ExCmds */
// TODO(saulrh): replace this with messaging to the background
export async function acceptExCmd(exstr: string): Promise<any> {
// TODO: Errors should go to CommandLine.
try {
let [func, args] = exmode_parser(exstr)
// Stop the repeat excmd from recursing.
if (func !== repeat) last_ex_str = exstr
try {
return await func(...args)
} catch (e) {
// Errors from func are caught here (e.g. no next tab)
logger.error("background_controller: ", e)
}
} catch (e) {
// Errors from parser caught here
logger.error("background_controller: ", e)
}
}
import * as Messaging from "./messaging"
// Get messages from content
Messaging.addListener(
"controller_background",
Messaging.attributeCaller({
acceptExCmd,
}),
)

Some files were not shown because too many files have changed in this diff Show more