Merge branch 'master' of github.com:cmcaine/tridactyl into configdocs

This commit is contained in:
Oliver Blanthorn 2018-08-29 10:58:34 +01:00
commit fd218e060b
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
38 changed files with 886 additions and 430 deletions

View file

@ -52,9 +52,9 @@ quickmark t https://github.com/cmcaine/tridactyl/issues?utf8=%E2%9C%93&q=sort%3A
"
" New reddit is bad
autocmd DocStart www.reddit.com js tri.excmds.urlmodify("-t www old")
autocmd DocStart 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 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:

View file

@ -1,5 +1,48 @@
# Tridactyl changelog
## Release 1.13.3 / 2018-08-21
* New features:
* Our command line now has more completions:
* Ex commands and ex aliases with a little bit of help for each command.
* Settings, showing their current value (currently does not support options within options)
* Rapid hint mode improvements:
* Less jank (particularly if you hold a key down)
* Most hint modes now have a rapid mode with `hint -q[flag]` and bound to `;g[key]`
* The divergence from Pentadactyl is because we already have `g;` bound to "switch to tab containing last used input field and focus it", which is my pet favourite command.
* `tab` is now an alias for `buffer` (I meant to add this months ago)
* Old features:
* More hint modes from Pentadactyl that no-one missed added:
* `;O`, `;W`, and `;T` pre-fill the command line with the hinted URL and open/tabopen/winopen.
* Added I, Shift-Escape ignore binds back
* You can unbind them with `unbind --mode=... [key]`
* Bug fixes:
* Yank element text hint mode was broken (`;p`) and we fixed it by accident
* You can now unbind keys which were bound to `Esc` by default (#921)
* Less console spam: fixed "1.1" error if config was at latest version
* Our command line now plays nicely with left scrollbars (#909)
* `guiset gui none` now pads maximised windows to fix a bug on Windows where the top of the page is cut off
* Boring internal changes:
* All hint modes now use a newfangled method with less message passing
* We're now using Typescript 3
* We now generate a bunch of metadata about Tridactyl for use with introspection
* As an added bonus, build times are now a bit slower.
Thanks to all of our contributors for this release: Oliver Blanthorn, glacambre, Anton Vilhelm Ásgeirsson, and Henré Botha.
Extra special thanks go to Henré Botha, who contributed for the first time.
Last, but not least - thank you to everyone who reported issues.
## Release 1.13.2 / 2018-08-15
* New features:

View file

@ -2,10 +2,99 @@ import * as ts from "typescript"
import * as fs from "fs"
import * as commandLineArgs from "command-line-args"
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]),
)
}
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}`,
)
}
}
}
/** True if this is visible outside this file, false otherwise */
function isNodeExported(node: ts.Node): boolean {
return (
(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0 ||
(ts.getCombinedModifierFlags(<ts.Declaration>node) &
ts.ModifierFlags.Export) !==
0 ||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
)
}
@ -33,7 +122,11 @@ function visit(checker: any, filename: string, node: any, everything: any) {
symbol,
symbol.valueDeclaration!,
)
if (ttype) nodeInfo["type"] = checker.typeToString(ttype)
if (ttype) {
nodeInfo["type"] = new SimpleType(
checker.typeToTypeNode(ttype),
)
}
}
break
// Other declaration syntaxkinds:
@ -61,7 +154,12 @@ function visit(checker: any, filename: string, node: any, everything: any) {
ts.forEachChild(node, node => visit(checker, filename, node, everything))
}
function generateMetadata(out: string, fileNames: string[]): void {
function generateMetadata(
out: string,
themedir: string,
fileNames: string[],
): void {
/* Parse Tridactyl */
let program = ts.createProgram(fileNames, {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
@ -74,23 +172,33 @@ function generateMetadata(out: string, fileNames: string[]): void {
if (n) visit(program.getTypeChecker(), n, sourceFile, everything)
}
let metadataString = `\nexport let everything = ${JSON.stringify(
everything,
undefined,
4,
)}\n`
if (themedir) {
metadataString += `\nexport let staticThemes = ${JSON.stringify(
fs.readdirSync(themedir),
)}\n`
}
// print out the doc
fs.writeFileSync(
out,
"export let everything = " + JSON.stringify(everything, undefined, 4),
)
fs.writeFileSync(out, metadataString)
return
}
let opts = commandLineArgs([
{ name: "out", type: String },
{ name: "themeDir", type: String },
{ name: "src", type: String, multiple: true, defaultOption: true },
])
if (!opts.out)
if (!opts.out || opts.src.length < 1)
throw new Error(
"Argument syntax: --out outfile [--src] file1.ts [file2.ts ...]",
)
generateMetadata(opts.out, opts.src)
generateMetadata(opts.out, opts.themeDir, opts.src)

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
source ./scripts/common

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -e
@ -22,11 +22,18 @@ manifest_loc="https://raw.githubusercontent.com/cmcaine/tridactyl/master/native/
native_loc="https://raw.githubusercontent.com/cmcaine/tridactyl/master/native/native_main.py"
# Decide where to put the manifest based on OS
if [[ "$OSTYPE" == "linux-gnu" ]]; then
case "$OSTYPE" in
linux-gnu|linux|freebsd*)
manifest_home="$HOME/.mozilla/native-messaging-hosts/"
elif [[ "$OSTYPE" == "darwin"* ]]; then
;;
darwin*)
manifest_home="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts/"
fi
;;
*)
# Fallback to default Linux location for unknown OSTYPE
manifest_home="$HOME/.mozilla/native-messaging-hosts/"
;;
esac
mkdir -p "$manifest_home" "$XDG_DATA_HOME"

132
package-lock.json generated
View file

@ -113,25 +113,42 @@
"resolved": "https://registry.npmjs.org/@types/css/-/css-0.0.31.tgz",
"integrity": "sha512-Xt3xp8o0zueqrdIkAOO5gy5YNs+jYfmIUPeoeKiwrcmCRXuNWkIgR2Ph6vHuVfi1y6c9Tx214EQBWPEkU97djw=="
},
"@types/events": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
"dev": true
},
"@types/fs-extra": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-4.0.0.tgz",
"integrity": "sha512-PlKJw6ujJXLJjbvB3T0UCbY3jibKM6/Ya5cc9j1q+mYDeK3aR4Dp+20ZwxSuvJr9mIoPxp7+IL4aMOEvsscRTA==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.4.tgz",
"integrity": "sha512-DsknoBvD8s+RFfSGjmERJ7ZOP1HI0UZRA3FSI+Zakhrc/Gy26YQsLI+m5V5DHxroHRJqCDLKJp7Hixn8zyaF7g==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/glob": {
"version": "5.0.35",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.35.tgz",
"integrity": "sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/minimatch": "*",
"@types/node": "*"
}
},
"@types/handlebars": {
"version": "4.0.31",
"resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.31.tgz",
"integrity": "sha1-p/umb6/kJxOu6I7sqNuRGS7+bnI=",
"version": "4.0.39",
"resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.39.tgz",
"integrity": "sha512-vjaS7Q0dVqFp85QhyPSZqDKnTTCemcSHNHFvDdalO1s0Ifz5KuE64jQD5xoUkfdWwF4WpqdJEl7LsWH8rzhKJA==",
"dev": true
},
"@types/highlight.js": {
"version": "9.1.8",
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.1.8.tgz",
"integrity": "sha1-0ifxi8uPPxh+FpZfJESFmgRol1g=",
"version": "9.12.3",
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.3.tgz",
"integrity": "sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ==",
"dev": true
},
"@types/jest": {
@ -141,21 +158,21 @@
"dev": true
},
"@types/lodash": {
"version": "4.14.74",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.74.tgz",
"integrity": "sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ==",
"version": "4.14.116",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.116.tgz",
"integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==",
"dev": true
},
"@types/marked": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz",
"integrity": "sha512-CSf9YWJdX1DkTNu9zcNtdCcn6hkRtB5ILjbhRId4ZOQqx30fXmdecuaXhugQL6eyrhuXtaHJ7PHI+Vm7k9ZJjg==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.4.0.tgz",
"integrity": "sha512-xkURX55US18wHme+O2UlqJf3Fo7FqT5VAL+OJ/zK+jP2NX57naryDHoiqt/pMIwZjDc62sRvXUWuQQxQiBdheQ==",
"dev": true
},
"@types/minimatch": {
"version": "2.0.29",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-2.0.29.tgz",
"integrity": "sha1-UALhT3Xi1x5WQoHfBDHIwbSio2o=",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"@types/nearley": {
@ -170,11 +187,12 @@
"dev": true
},
"@types/shelljs": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.7.0.tgz",
"integrity": "sha1-IpwVfGvB5n1rmQ5sXhjb0v9Yz/A=",
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.0.tgz",
"integrity": "sha512-vs1hCC8RxLHRu2bwumNyYRNrU3o8BtZhLysH5A4I98iYmA2APl6R3uNQb5ihl+WiwH0xdC9LLO+vRrXLs/Kyxg==",
"dev": true,
"requires": {
"@types/glob": "*",
"@types/node": "*"
}
},
@ -11800,47 +11818,51 @@
"dev": true
},
"typedoc": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.9.0.tgz",
"integrity": "sha512-numP0CtcUK4I1Vssw6E1N/FjyJWpWqhLT4Zb7Gw3i7ca3ElnYh6z41Y/tcUhMsMYn6L8b67E/Fu4XYYKkNaLbA==",
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.12.0.tgz",
"integrity": "sha512-dsdlaYZ7Je8JC+jQ3j2Iroe4uyD0GhqzADNUVyBRgLuytQDP/g0dPkAw5PdM/4drnmmJjRzSWW97FkKo+ITqQg==",
"dev": true,
"requires": {
"@types/fs-extra": "4.0.0",
"@types/handlebars": "4.0.31",
"@types/highlight.js": "9.1.8",
"@types/lodash": "4.14.74",
"@types/marked": "0.3.0",
"@types/minimatch": "2.0.29",
"@types/shelljs": "0.7.0",
"fs-extra": "^4.0.0",
"@types/fs-extra": "^5.0.3",
"@types/handlebars": "^4.0.38",
"@types/highlight.js": "^9.12.3",
"@types/lodash": "^4.14.110",
"@types/marked": "^0.4.0",
"@types/minimatch": "3.0.3",
"@types/shelljs": "^0.8.0",
"fs-extra": "^7.0.0",
"handlebars": "^4.0.6",
"highlight.js": "^9.0.0",
"lodash": "^4.13.1",
"marked": "^0.3.5",
"lodash": "^4.17.10",
"marked": "^0.4.0",
"minimatch": "^3.0.0",
"progress": "^2.0.0",
"shelljs": "^0.7.0",
"shelljs": "^0.8.2",
"typedoc-default-themes": "^0.5.0",
"typescript": "2.4.1"
"typescript": "3.0.x"
},
"dependencies": {
"marked": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz",
"integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==",
"dev": true
"fs-extra": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz",
"integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"progress": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
"integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
"dev": true
},
"typescript": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.1.tgz",
"integrity": "sha1-w8yxbdqgsjFN4DHn5v7onlujRrw=",
"dev": true
"shelljs": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.2.tgz",
"integrity": "sha512-pRXeNrCA2Wd9itwhvLp5LZQvPJ0wU6bcjaTMywHHGX5XWhVN2nzSu7WV0q+oUY7mGK3mgSkDDzP3MgjqdyIgbQ==",
"dev": true,
"requires": {
"glob": "^7.0.0",
"interpret": "^1.0.0",
"rechoir": "^0.6.2"
}
}
}
},
@ -11850,9 +11872,9 @@
"dev": true
},
"typescript": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz",
"integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz",
"integrity": "sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==",
"dev": true
},
"typical": {

View file

@ -27,9 +27,9 @@
"source-map-loader": "^0.2.2",
"ts-jest": "^21.1.3",
"ts-node": "^3.3.0",
"typedoc": "^0.9.0",
"typedoc": "^0.12.0",
"typedoc-default-themes": "git://github.com/glacambre/typedoc-default-themes#fix_weird_member_names_bin",
"typescript": "^2.5.3",
"typescript": "^3.0.1",
"uglify-es": "^3.1.5",
"uglifyjs-webpack-plugin": "^1.2.5",
"web-ext": "^2.7.0",

View file

@ -8,7 +8,7 @@ Replace Firefox's default control mechanism with one modelled on the one true ed
## Installing
[Get our "beta" builds!][betas] These 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 [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.
Type `:help` or press `<F1>` for online help once you're in :)
@ -363,4 +363,5 @@ The logo was designed by Jake Beazley using free vector art by <a target="_blank
[matrix-badge]: https://matrix.to/img/matrix-badge.svg
[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
[amo]: https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim?src=external-github

View file

@ -38,7 +38,10 @@ fi
# 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 \
&& node compiler/gen_metadata.js --out src/.metadata.generated.ts src/*.ts
&& node compiler/gen_metadata.js \
--out src/.metadata.generated.ts \
--themeDir src/static/themes \
src/*.ts
scripts/newtab.md.sh
scripts/make_tutorial.sh

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Accepts no arguments
# Returns git-add'ed files as a list of filenames separated by a newline character

View file

@ -1,27 +1,52 @@
#!/bin/bash
#!/usr/bin/env bash
# Run prettier on each staged file that needs it without touching the working tree
source ./scripts/common
set -e
set -Eeuo pipefail
uglyFiles="$(ugly "$(cachedFiles)")"
echoe() {
echo "$@" >&2
}
if [ -n "$uglyFiles" ]; then
IFS=$'\n'
for file in $uglyFiles; do
if cmp -s <(staged "$file") "$file"; then
prettier --write "$file"
git add "$file"
lock() {
local lockfile="$1"
if [ -e "$lockfile" ]; then
echoe "Lockfile '$lockfile' already exists. Check no other operation is occuring or delete lockfile"
return 1
else
echo "WARN: Staged and working file differ: '$file'"
echo "WARN: Moving working file temporarily (this might upset your editor)"
# Get crazy: backup the working copy of the file, paste the staged version in its place, prettify, add, restore backup
backup=$(mktemp -p . "$file.XXXXXXXXX")
mv "$file" "$backup"
staged "$file" | prettier --stdin-filepath "$file" > "$file"
git add "$file"
mv "$backup" "$file"
echo "WARN: Working file restored"
touch "$lockfile"
fi
}
unlock() {
rm "$1"
}
trap "unlock $(git rev-parse --show-toplevel)/.git/index.lock || true" ERR
main() {
local stagedFiles=$(cachedFiles)
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
(
cd "$tmpdir"
mkdir -p $(dirname $file)
lock ../.git/index.lock
staged "$file" | prettier --stdin-filepath "$file" > "$file"
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"
)
done
fi
rm -rf "$tmpdir"
fi
}
main

View file

@ -1,4 +1,4 @@
#!/bin/bash -e
#!/usr/bin/env bash -e
# This script must be run from the root Tridactyl directory
TRIDIR="$(pwd)"

View file

@ -34,3 +34,23 @@ export function expandExstr(
prevExpansions,
)
}
/**
* Get all aliases for all commands.
*
* @param aliases An object mapping aliases to commands
* @return commands An object mapping commands to an array of aliases
*/
export function getCmdAliasMapping(
aliases = config.get("exaliases"),
): { [str: string]: string[] } {
let commands = {}
// aliases look like this: {alias: command} but what we really need is this: {command: [alias1, alias2...]}
// This is what this loop builds
for (let alias in aliases) {
let cmd = expandExstr(alias, aliases).trim()
if (!commands[cmd]) commands[cmd] = []
commands[cmd].push(alias.trim())
}
return commands
}

View file

@ -8,13 +8,13 @@ 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 * as aliases from "./aliases"
import { theme } from "./styling"
const logger = new Logger("cmdline")
@ -59,6 +59,7 @@ function enableCompletions() {
new BufferAllCompletionSource(completionsDiv),
new BufferCompletionSource(completionsDiv),
new ExcmdCompletionSource(completionsDiv),
new SettingsCompletionSource(completionsDiv),
new HistoryCompletionSource(completionsDiv),
]
@ -207,13 +208,12 @@ clInput.addEventListener("keydown", function(keyevent) {
clInput.addEventListener("input", () => {
const exstr = clInput.value
const expandedCmd = aliases.expandExstr(exstr)
// Fire each completion and add a callback to resize area
enableCompletions()
logger.debug(activeCompletions)
activeCompletions.forEach(comp =>
comp.filter(expandedCmd).then(() => resizeArea()),
comp.filter(exstr).then(() => resizeArea()),
)
})

View file

@ -13,6 +13,8 @@ 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"
export const DEFAULT_FAVICON = browser.extension.getURL(
"static/defaultFavicon.svg",
@ -33,6 +35,20 @@ export abstract class CompletionSource {
readonly options: CompletionOption[]
node: HTMLElement
public completion: string
protected prefixes: string[] = []
constructor(prefixes) {
let 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])
})
// Not sure this is necessary but every completion source has it
this.prefixes = this.prefixes.map(p => p + " ")
}
/** Update [[node]] to display completions relevant to exstr */
public abstract filter(exstr: string): Promise<void>
@ -117,8 +133,8 @@ export abstract class CompletionSourceFuse extends CompletionSource {
protected optionContainer = html`<table class="optionContainer">`
constructor(private prefixes, className: string, title?: string) {
super()
constructor(prefixes, className: string, title?: string) {
super(prefixes)
this.node = html`<div class="${className} hidden">
<div class="sectionHeader">${title || className}</div>
</div>`

View file

@ -36,7 +36,7 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
public options: BmarkCompletionOption[]
constructor(private _parent) {
super(["bmarks "], "BmarkCompletionSource", "Bookmarks")
super(["bmarks"], "BmarkCompletionSource", "Bookmarks")
this._parent.appendChild(this.node)
}

View file

@ -57,7 +57,7 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
constructor(private _parent) {
super(
["buffer ", "tabclose ", "tabdetach ", "tabduplicate ", "tabmove "],
["buffer", "tabclose", "tabdetach", "tabduplicate", "tabmove"],
"BufferCompletionSource",
"Buffers",
)

View file

@ -42,7 +42,7 @@ export class BufferAllCompletionSource extends Completions.CompletionSourceFuse
public options: BufferAllCompletionOption[]
constructor(private _parent) {
super(["bufferall "], "BufferAllCompletionSource", "All Buffers")
super(["bufferall"], "BufferAllCompletionSource", "All Buffers")
this.updateOptions()
this._parent.appendChild(this.node)

View file

@ -1,14 +1,17 @@
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"
class ExcmdCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(
public value: string,
public ttype: string,
public documentation: string,
public ttype: string = "",
public documentation: string = "",
) {
super()
this.fuseKeys.push(this.value)
@ -16,10 +19,10 @@ class ExcmdCompletionOption extends Completions.CompletionOptionHTML
// Create HTMLElement
this.html = html`<tr class="ExcmdCompletionOption option">
<td class="excmd">${value}</td>
<td class="type">${ttype}</td>
<td class="documentation">${documentation}</td>
</tr>`
}
// <td class="type">${ttype}</td>
}
export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
@ -50,29 +53,56 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
this.options = (await this.scoreOptions(
Object.keys(fns).filter(f => f.startsWith(exstr)),
)).map(f => {
try {
return new ExcmdCompletionOption(f, fns[f].type, fns[f].doc)
} catch {
return new ExcmdCompletionOption("", "", "")
}
let t = ""
if (fns[f].type) t = typeToSimpleString(fns[f].type)
return new ExcmdCompletionOption(f, t, fns[f].doc)
})
let exaliases = config.get("exaliases")
for (let alias of Object.keys(exaliases).filter(a =>
a.startsWith(exstr),
)) {
let cmd = aliases.expandExstr(alias)
if (fns[cmd]) {
this.options = this.options.concat(
new ExcmdCompletionOption(
alias,
fns[cmd].type ? typeToSimpleString(fns[cmd].type) : "",
`Alias for \`${cmd}\`. ${fns[cmd].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.options.forEach(o => (o.state = "normal"))
this.updateChain()
}
private async scoreOptions(exstrs: string[]) {
let histpos = state.cmdHistory.map(s => s.split(" ")[0]).reverse()
return exstrs.sort((a, b) => {
let posa = histpos.findIndex(x => x == a)
let posb = histpos.findIndex(x => x == b)
// If two ex commands have the same position, sort lexically
if (posa == posb) return a < b ? -1 : 1
// If they aren't found in the list they get lower priority
if (posa == -1) return 1
if (posb == -1) return -1
// Finally, sort by history position
return posa < posb ? -1 : 1
})
return exstrs.sort()
// Too slow with large profiles
// let histpos = state.cmdHistory.map(s => s.split(" ")[0]).reverse()
// return exstrs.sort((a, b) => {
// let posa = histpos.findIndex(x => x == a)
// let posb = histpos.findIndex(x => x == b)
// // If two ex commands have the same position, sort lexically
// if (posa == posb) return a < b ? -1 : 1
// // If they aren't found in the list they get lower priority
// if (posa == -1) return 1
// if (posb == -1) return -1
// // Finally, sort by history position
// return posa < posb ? -1 : 1
// })
}
select(option: ExcmdCompletionOption) {

View file

@ -35,7 +35,7 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
constructor(private _parent) {
super(
["open ", "tabopen ", "winopen "],
["open", "tabopen", "winopen"],
"HistoryCompletionSource",
"History",
)

View file

@ -0,0 +1,72 @@
import * as Completions from "../completions"
import * as config from "../config"
import { browserBg } from "../lib/webext"
class SettingsCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(
public value: string,
setting: { name: string; value: string; docs: string },
) {
super()
this.html = html`<tr class="SettingsCompletionOption option">
<td class="title">${setting.name}</td>
<td class="content">${setting.value}</td>
</tr>`
}
}
export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
public options: SettingsCompletionOption[]
constructor(private _parent) {
super(["set", "get"], "SettingsCompletionSource", "Settings")
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
this.lastExstr = exstr
let [prefix, query] = this.splitOnPrefix(exstr)
let options = ""
// 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
}
let settings = config.get()
this.options = Object.keys(settings)
.filter(x => x.startsWith(query))
.sort()
.map(
setting =>
new SettingsCompletionOption(setting, {
name: setting,
value: JSON.stringify(settings[setting]),
docs: "",
}),
)
// this.options = [new SettingsCompletionOption("ok", {name: "ok", docs:""})]
this.updateChain()
}
updateChain() {
// Options are pre-trimmed to the right length.
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
this.updateDisplay()
}
onInput() {}
}

View file

@ -192,7 +192,7 @@ config.getAsync("leavegithubalone").then(v => {
// 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) {
if ("/".indexOf(e.key) != -1 && state.mode == "normal") {
e.cancelBubble = true
e.stopImmediatePropagation()
}
@ -203,7 +203,7 @@ config.getAsync("leavegithubalone").then(v => {
window.addEventListener("DOMContentLoaded", () => {
// if (document.location.host == "github.com") {
document.body.addEventListener("keydown", function(e) {
if ("/".indexOf(e.key) != -1) {
if ("/".indexOf(e.key) != -1 && state.mode == "normal") {
e.cancelBubble = true
e.stopImmediatePropagation()
}

View file

@ -111,6 +111,13 @@ export const potentialRules = {
show: ``,
},
},
padwhenmaximised: {
name: `#main-window[sizemode="maximized"] #content-deck`,
options: {
some: `padding-top: 8px;`,
none: ``,
},
},
}
// Vimperator's options for reference:
@ -141,6 +148,7 @@ export const metaRules = {
navbar: "autohide",
titlebar: "hide",
menubar: "grey",
padwhenmaximised: "some",
},
full: {
hoverlink: "left",
@ -148,6 +156,7 @@ export const metaRules = {
navbar: "always",
titlebar: "show",
menubar: "default",
padwhenmaximised: "none",
},
},
tabs: {

View file

@ -95,7 +95,7 @@
// Shared
import * as Messaging from "./messaging"
import { browserBg, activeTabId, activeTabContainerId, openInNewTab } from "./lib/webext"
import { browserBg, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow } from "./lib/webext"
import * as Container from "./lib/containers"
import state from "./state"
import * as UrlUtil from "./url_util"
@ -851,7 +851,7 @@ export async function reloadhard(n = 1) {
// I went through the whole list https://developer.mozilla.org/en-US/Firefox/The_about_protocol
// about:blank is even more special
/** @hidden */
export const ABOUT_WHITELIST = ["about:home", "about:license", "about:logo", "about:rights"]
export const ABOUT_WHITELIST = ["about:license", "about:logo", "about:rights"]
/** Open a new page in the current tab.
*
@ -1602,6 +1602,7 @@ export async function tabopen(...addressarr: string[]) {
let url: string
let address = (await argParse(addressarr)).join(" ")
if (address == "") address = config.get("newtab")
if (!ABOUT_WHITELIST.includes(address) && address.match(/^(about|file):.*/)) {
if ((await browser.runtime.getPlatformInfo()).os === "mac" && (await browser.windows.getCurrent()).incognito) {
fillcmdline_notrail("# nativeopen isn't supported in private mode on OSX. Consider installing Linux or Windows :).")
@ -1611,7 +1612,6 @@ export async function tabopen(...addressarr: string[]) {
return
}
} else if (address != "") url = forceURI(address)
else url = forceURI(config.get("newtab"))
activeTabContainerId().then(containerId => {
// Ensure -c has priority.
@ -1762,17 +1762,24 @@ export async function tabclosealltoleft() {
//#background
export async function undo() {
const current_win_id: number = (await browser.windows.getCurrent()).id
const sessions = await browser.sessions.getRecentlyClosed()
const sessions = await browser.sessions.getRecentlyClosed({ maxResults: 10 })
// The first session object that's a window or a tab from this window. Or undefined if sessions is empty.
let closed = sessions.find(s => {
return "window" in s || (s.tab && s.tab.windowId == current_win_id)
const lastSession = sessions.find(s => {
if (s.window) {
return true
} else if (s.tab && s.tab.windowId === current_win_id) {
return true
} else {
return false
}
})
if (closed) {
if (closed.tab) {
browser.sessions.restore(closed.tab.sessionId)
} else if (closed.window) {
browser.sessions.restore(closed.window.sessionId)
if (lastSession) {
if (lastSession.tab) {
browser.sessions.restore(lastSession.tab.sessionId)
} else if (lastSession.window) {
browser.sessions.restore(lastSession.window.sessionId)
}
}
}
@ -2892,7 +2899,6 @@ import * as hinting from "./hinting"
@param option
- -b open in background
- -br repeatedly open in background
- -y copy (yank) link's target to clipboard
- -p copy an element's text to the clipboard
- -P copy an element's title/alt text to the clipboard
@ -2909,14 +2915,13 @@ import * as hinting from "./hinting"
- -c [selector] hint links that match the css selector
- `bind ;c hint -c [class*="expand"],[class="togg"]` works particularly well on reddit and HN
- -w open in new window
-wp open in new private window
- `-pipe selector key` e.g, `-pipe * href` returns the key. Only makes sense with `composite`, e.g, `composite hint -pipe * textContent | yank`.
- **DEPRECATED** `-W excmd...` append hint href to excmd and execute, e.g, `hint -W exclaim mpv` to open YouTube videos. Use `composite hint -pipe | [excmd]` instead.
- -wp open in new private window
- `-pipe selector key` e.g, `-pipe * href` returns the key. Only makes sense with `composite`, e.g, `composite hint -pipe * textContent | yank`. If you don't select a hint (i.e. press <Esc>), will return an empty string.
- `-W excmd...` append hint href to excmd and execute, e.g, `hint -W exclaim mpv` to open YouTube videos.
- -q* quick (or rapid) hints mode. Stay in hint mode until you press <Esc>, e.g. `:hint -qb` to open multiple hints in the background or `:hint -qW excmd` to execute excmd once for each hint. This will return an array containing all elements or the result of executed functions (e.g. `hint -qpipe a href` will return an array of links).
- -br deprecated, use `-qb` instead
Excepting the custom selector mode and background hint mode, each of these
hint modes is available by default as `;<option character>`, so e.g. `;y`
to yank a link's target.
Excepting the custom selector mode and background hint mode, each of these hint modes is available by default as `;<option character>`, so e.g. `;y` to yank a link's target; `;g<option character>` starts rapid hint mode for all modes where it makes sense, and some others.
To open a hint in the background, the default bind is `F`.
@ -2943,150 +2948,257 @@ import * as hinting from "./hinting"
*/
//#content
export async function hint(option?: string, selectors?: string, ...rest: string[]) {
// NB: if you want something to work with rapid hinting, make it return a tuple of [something, hintCount] see option === "-b" below.
let selectHints = new Promise(r => r())
let onSelected = a => a
if (!option) option = ""
// Open in background
if (option === "-b") {
selectHints = hinting.pipe(DOM.HINTTAGS_selectors)
onSelected = async result => {
let [link, hintCount] = result as [HTMLAnchorElement, number]
link.focus()
if (link.href) {
if (option == "-br") option = "-qb"
let rapid = false
if (option.startsWith("-q")) {
option = "-" + option.slice(2)
rapid = true
}
let selectHints = new Promise(r => r())
let hintTabOpen = async (href, active = !rapid) => {
let containerId = await activeTabContainerId()
if (containerId) {
openInNewTab(link.href, {
active: false,
return await openInNewTab(href, {
active,
related: true,
cookieStoreId: containerId,
}).catch(() => DOM.simulateClick(link))
})
} else {
openInNewTab(link.href, {
active: false,
return await openInNewTab(href, {
active,
related: true,
}).catch(() => DOM.simulateClick(link))
})
}
}
switch (option) {
case "-b":
// Open in background
selectHints = hinting.pipe(
DOM.HINTTAGS_selectors,
async link => {
link.focus()
if (link.href) {
hintTabOpen(link.href, false).catch(() => DOM.simulateClick(link))
} else {
DOM.simulateClick(link)
}
return [link.href, hintCount]
}
}
return link
},
rapid,
)
break
case "-y":
// Yank link
else if (option === "-y") {
selectHints = hinting.pipe(DOM.HINTTAGS_selectors)
onSelected = result => {
selectHints = hinting.pipe(
DOM.HINTTAGS_selectors,
elem => {
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + result[0]["href"])
return result
}
}
run_exstr("yank " + elem["href"])
return elem
},
rapid,
)
break
case "-p":
// Yank text content
else if (option === "-p") {
selectHints = hinting.pipe_elements(DOM.elementsWithText())
onSelected = result => {
selectHints = hinting.pipe_elements(
DOM.elementsWithText(),
elem => {
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + result["textContent"])
return result
}
}
run_exstr("yank " + elem["textContent"])
return elem
},
rapid,
)
break
case "-P":
// Yank link alt text
// ???: Neither anchors nor links posses an "alt" attribute. I'm assuming that the person who wrote this code also wanted to select the alt text of images
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link
else if (option === "-P") {
selectHints = hinting.pipe_elements(DOM.getElemsBySelector("[title], [alt]", [DOM.isVisible]))
onSelected = result => {
let link = result[0] as HTMLAnchorElement & HTMLImageElement
selectHints = hinting.pipe_elements(
DOM.getElemsBySelector("[title], [alt]", [DOM.isVisible]),
link => {
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + (link.title ? link.title : link.alt))
return result
}
}
return link
},
rapid,
)
break
case "-#":
// Yank anchor
else if (option === "-#") {
selectHints = hinting.pipe_elements(DOM.anchors())
onSelected = result => {
selectHints = hinting.pipe_elements(
DOM.anchors(),
link => {
let anchorUrl = new URL(window.location.href)
let link = result[0] as any
// ???: What purpose does selecting elements with a name attribute have? Selecting values that only have meaning in forms doesn't seem very useful.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
anchorUrl.hash = link.id || link.name
// /!\ Warning: This is racy! This can easily be fixed by adding an await but do we want this? yank can be pretty slow, especially with yankto=selection
run_exstr("yank " + anchorUrl.href)
return result
}
} else if (option === "-c") {
selectHints = hinting.pipe(selectors)
onSelected = result => {
DOM.simulateClick(result[0] as HTMLElement)
return result
}
}
// Deprecated: hint exstr
else if (option === "-W") {
selectHints = hinting.pipe(DOM.HINTTAGS_selectors)
onSelected = result => {
// /!\ RACY RACY RACY!
run_exstr(selectors + " " + rest.join(" ") + " " + result[0])
return result
}
} else if (option === "-pipe") {
selectHints = hinting.pipe(selectors)
onSelected = result => result[0][rest.join(" ")]
} else if (option === "-br") {
while (true) {
// The typecast can be removed once the function is completely ported
let result = (await hint("-b")) as [HTMLElement, number]
if (result === null) return null
let [_, hintCount] = result
if (hintCount < 2) break
}
}
// TODO: port these to new fangled way
else if (option === "-i") hinting.hintImage(false)
else if (option === "-I") hinting.hintImage(true)
else if (option === "-k") hinting.hintKill()
else if (option === "-s") hinting.hintSave("link", false)
else if (option === "-S") hinting.hintSave("img", false)
else if (option === "-a") hinting.hintSave("link", true)
else if (option === "-A") hinting.hintSave("img", true)
else if (option === "-;") hinting.hintFocus(selectors)
else if (option === "-r") hinting.hintRead()
else if (option === "-w") hinting.hintPageWindow()
else if (option === "-wp") hinting.hintPageWindowPrivate()
else {
selectHints = hinting.pipe(DOM.HINTTAGS_selectors)
onSelected = result => {
DOM.simulateClick(result[0] as HTMLElement)
return result
}
}
return new Promise((resolve, reject) =>
selectHints.then(
async result => resolve(await onSelected(result)),
rejectionReason => {
// We have to resolve when we don't want to have our messages be logged in the command line but this feels wrong since no hint has been selected
// Perhaps we should implement a mechanism to allow specific errors to go unreported?
if (rejectionReason == hinting.HintRejectionReason.User) {
logger.debug("Hint promise rejected because user left hint mode without selecting a hint")
resolve(null)
} else if (rejectionReason == hinting.HintRejectionReason.NoHints) {
logger.debug("Hint promise rejected because there are no hints to select")
resolve(null)
} else {
reject(rejectionReason)
}
return link
},
),
rapid,
)
break
case "-c":
selectHints = hinting.pipe(
selectors,
elem => {
DOM.simulateClick(elem as HTMLElement)
return elem
},
rapid,
)
break
case "-W":
selectHints = hinting.pipe(
DOM.HINTTAGS_selectors,
elem => {
// /!\ RACY RACY RACY!
run_exstr(selectors + " " + rest.join(" ") + " " + elem)
return elem
},
rapid,
)
break
case "-pipe":
selectHints = hinting.pipe(selectors, elem => elem[rest.join(" ")], rapid)
break
case "-i":
selectHints = hinting.pipe_elements(
hinting.hintableImages(),
elem => {
open(new URL(elem.getAttribute("src"), window.location.href).href)
return elem
},
rapid,
)
break
case "-I":
selectHints = hinting.pipe_elements(
hinting.hintableImages(),
async elem => {
await hintTabOpen(new URL(elem.getAttribute("src"), window.location.href).href)
return elem
},
rapid,
)
break
case "-k":
selectHints = hinting.pipe_elements(
hinting.killables(),
elem => {
elem.remove()
return elem
},
rapid,
)
break
case "-s":
case "-a":
case "-S":
case "-A":
let elems = []
// s: don't ask the user where to save the file
// a: ask the user where to save the file
let saveAs = true
if (option[1].toLowerCase() == "s") saveAs = false
// Lowercase: anchors
// Uppercase: images
let attr = "href"
if (option[1].toLowerCase() == option[1]) {
attr = "href"
elems = hinting.saveableElements()
} else {
attr = "src"
elems = hinting.hintableImages()
}
selectHints = hinting.pipe_elements(
elems,
elem => {
Messaging.message("download_background", "downloadUrl", [new URL(elem[attr], window.location.href).href, saveAs])
return elem
},
rapid,
)
break
case "-;":
selectHints = hinting.pipe_elements(
hinting.hintables(selectors),
elem => {
elem.focus()
return elem
},
rapid,
)
break
case "-r":
selectHints = hinting.pipe_elements(
DOM.elementsWithText(),
elem => {
TTS.readText(elem.textContent)
return elem
},
rapid,
)
break
case "-w":
selectHints = hinting.pipe_elements(
hinting.hintables(),
elem => {
elem.focus()
if (elem.href) openInNewWindow({ url: new URL(elem.href, window.location.href).href })
else DOM.simulateClick(elem)
return elem
},
rapid,
)
break
case "-wp":
selectHints = hinting.pipe_elements(
hinting.hintables(),
elem => {
elem.focus()
if (elem.href) return openInNewWindow({ url: elem.href, incognito: true })
},
rapid,
)
break
default:
selectHints = hinting.pipe(
DOM.HINTTAGS_selectors,
elem => {
DOM.simulateClick(elem as HTMLElement)
return elem
},
rapid,
)
}
return selectHints
}
// how 2 crash pc

View file

@ -28,31 +28,25 @@ import Logger from "./logging"
import * as Messaging from "./messaging"
const logger = new Logger("hinting")
export enum HintRejectionReason {
Err,
User,
NoHints,
}
/** Simple container for the state of a single frame's hints. */
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: (Hint) => void,
public reject: (HintRejectionReason) => any,
public reject: (any) => void,
public rapid: boolean,
) {
this.hintHost.classList.add("TridactylHintHost", "cleanslate")
}
destructor(abort) {
if (!this.focusedHint) this.focusedHint = this.hints[0]
destructor() {
// Undo any alterations of the hinted elements
for (const hint of this.hints) {
hint.hidden = true
@ -61,9 +55,11 @@ class HintState {
// Remove all hints from the DOM.
this.hintHost.remove()
if (abort) this.reject(HintRejectionReason.User)
else if (!this.focusedHint) this.reject(HintRejectionReason.NoHints)
else this.resolve([this.focusedHint.target, this.hints.length])
if (this.rapid) this.resolve(this.selectedHints.map(h => h.result))
else
this.resolve(
this.selectedHints[0] ? this.selectedHints[0].result : "",
)
}
}
@ -75,13 +71,25 @@ export function hintPage(
onSelect: HintSelectedCallback,
resolve = () => {},
reject = () => {},
rapid = false,
) {
let buildHints: HintBuilder = defaultHintBuilder()
let filterHints: HintFilter = defaultHintFilter()
state.mode = "hint"
modeState = new HintState(filterHints, resolve, reject)
modeState = new HintState(filterHints, resolve, reject, rapid)
buildHints(hintableElements, onSelect)
if (rapid == false) {
buildHints(hintableElements, hint => {
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) {
let firstTarget = modeState.hints[0].target
@ -227,6 +235,7 @@ type HintSelectedCallback = (Hint) => any
/** Place a flag by each hintworthy element */
class Hint {
public readonly flag = document.createElement("span")
public result: any = null
constructor(
public readonly target: Element,
@ -444,8 +453,10 @@ function filterHintsVimperator(fstr, reflow = false) {
* If abort is true, we're resetting because the user pressed escape.
* If it is false, we're resetting because the user selected a hint.
**/
function reset(abort = false) {
modeState.destructor(abort)
function reset() {
if (modeState) {
modeState.destructor()
}
modeState = undefined
state.mode = "normal"
}
@ -473,7 +484,7 @@ function pushKey(ke) {
1. Within viewport
2. Not hidden by another element
*/
function hintables(selectors = DOM.HINTTAGS_selectors, withjs = false) {
export function hintables(selectors = DOM.HINTTAGS_selectors, withjs = false) {
let elems = DOM.getElemsBySelector(selectors, [])
if (withjs) {
elems = elems.concat(DOM.hintworthy_js_elems)
@ -484,19 +495,19 @@ function hintables(selectors = DOM.HINTTAGS_selectors, withjs = false) {
/** Returns elements that point to a saveable resource
*/
function saveableElements() {
export function saveableElements() {
return DOM.getElemsBySelector(DOM.HINTTAGS_saveable, [DOM.isVisible])
}
/** Get array of images in the viewport
*/
function hintableImages() {
export function hintableImages() {
return DOM.getElemsBySelector(DOM.HINTTAGS_img_selectors, [DOM.isVisible])
}
/** Array of items that can be killed with hint kill
*/
function killables() {
export function killables() {
return DOM.getElemsBySelector(DOM.HINTTAGS_killable_selectors, [
DOM.isVisible,
])
@ -505,130 +516,33 @@ function killables() {
import { openInNewTab, activeTabContainerId } from "./lib/webext"
import { openInNewWindow } from "./lib/webext"
export function hintPageWindow() {
hintPage(hintables(), hint => {
hint.target.focus()
if (hint.target.href) {
openInNewWindow({ url: hint.target.href })
} else {
// This is to mirror vimperator behaviour.
DOM.simulateClick(hint.target)
}
})
}
export function hintPageWindowPrivate() {
hintPage(hintables(), hint => {
hint.target.focus()
if (hint.target.href) {
openInNewWindow({ url: hint.target.href, incognito: true })
}
})
}
export function pipe(
selectors = DOM.HINTTAGS_selectors,
action: HintSelectedCallback = _ => _,
rapid = false,
): Promise<[Element, number]> {
return new Promise((resolve, reject) => {
hintPage(hintables(selectors, true), () => {}, resolve, reject)
hintPage(hintables(selectors, true), action, resolve, reject, rapid)
})
}
export function pipe_elements(
elements: any = DOM.elementsWithText,
action: HintSelectedCallback = _ => _,
rapid = false,
): Promise<[Element, number]> {
return new Promise((resolve, reject) => {
hintPage(elements, () => {}, resolve, reject)
})
}
/** Hint images, opening in the same tab, or in a background tab
*
* @param inBackground opens the image source URL in a background tab,
* as opposed to the current tab
*/
export function hintImage(inBackground) {
hintPage(hintableImages(), hint => {
let img_src = hint.target.getAttribute("src")
if (inBackground) {
openInNewTab(new URL(img_src, window.location.href).href, {
active: false,
related: true,
})
} else {
window.location.href = img_src
}
})
}
/** Hint elements to focus */
export function hintFocus(selectors?) {
hintPage(hintables(selectors), hint => {
hint.target.focus()
})
}
/** Hint items and read out the content of the selection */
export function hintRead() {
hintPage(DOM.elementsWithText(), hint => {
TTS.readText(hint.target.textContent)
})
}
/** Hint elements and delete the selection from the page
*/
export function hintKill() {
hintPage(killables(), hint => {
hint.target.remove()
})
}
/** Type for "hint save" actions:
* - "link": elements that point to another resource (eg
* links to pages/files) - the link target is saved
* - "img": image elements
*/
export type HintSaveType = "link" | "img"
/** Hint link elements to save
*
* @param hintType the type of elements to hint and save:
* - "link": elements that point to another resource (eg
* links to pages/files) - the link targer is saved
* - "img": image elements
* @param saveAs prompt for save location
*/
export function hintSave(hintType: HintSaveType, saveAs: boolean) {
function saveHintElems(hintType) {
return hintType === "link" ? saveableElements() : hintableImages()
}
function urlFromElem(hintType, elem) {
return hintType === "link" ? elem.href : elem.src
}
hintPage(saveHintElems(hintType), hint => {
const urlToSave = new URL(
urlFromElem(hintType, hint.target),
window.location.href,
)
// Pass to background context to allow saving from data URLs.
// Convert to href because can't clone URL across contexts
message("download_background", "downloadUrl", [urlToSave.href, saveAs])
hintPage(elements, action, resolve, reject, rapid)
})
}
function selectFocusedHint(delay = false) {
logger.debug("Selecting hint.", state.mode)
const focused = modeState.focusedHint
let select = () => {
reset()
focused.select()
}
if (delay) setTimeout(select, config.get("hintdelay"))
else select()
modeState.filter = ""
modeState.hints.forEach(h => (h.hidden = false))
if (delay) setTimeout(() => focused.select(), config.get("hintdelay"))
else focused.select()
}
import { addListener, attributeCaller } from "./messaging"

View file

@ -300,11 +300,6 @@ export function mapstrMapToKeyMap(mapstrMap: Map<string, MapTarget>): KeyMap {
return newKeyMap
}
export function mapstrObjToKeyMap(mapstrObj): KeyMap {
const mapstrMap = new Map(Object.entries(mapstrObj))
return mapstrMapToKeyMap(mapstrMap)
}
// }}}
// {{{ Utility functions for dealing with KeyboardEvents

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Tridactyl",
"version": "1.13.2",
"version": "1.13.3",
"icons": {
"64": "static/logo/Tridactyl_64px.png",
"100": "static/logo/Tridactyl_100px.png",

68
src/metadata.ts Normal file
View file

@ -0,0 +1,68 @@
// Gets the type sitting inside a promise
function unwrapPromise(type) {
while (type.kind == "Promise") type = type.arguments[0]
return type
}
// This turns TYPE into a string, the format of which is quite short in order to be useful for completions
export function typeToSimpleString(type): string {
let result = ""
switch (type.kind) {
case "function":
result = type.arguments.map(typeToSimpleString).join(", ")
let t = unwrapPromise(type.type)
if (!["any", "object", "void"].includes(t.kind)) {
if (result == "") result = "()"
result += "->" + typeToString(t)
}
break
default:
result = type.name || ""
}
return result
}
// This turns TYPE into a string, the format of which is quite close to the one tsc uses to display type strings
export function typeToString(type): string {
let allTypes = arr => arr.map(typeToString).join(", ")
let result = ""
switch (type.kind) {
case "function":
result =
"(" +
allTypes(type.arguments) +
")" +
" => " +
typeToString(type.type)
break
case "array":
result = typeToString(type.type) + "[]"
break
case "Promise":
result = "Promise<" + allTypes(type.arguments) + ">"
break
case "union":
result = type.arguments.map(typeToString).join(" | ")
break
case "LiteralType":
result = type.name
break
case "tuple":
result = "(" + type.arguments.map(typeToString).join(", ") + ")"
case "any":
case "object":
case "boolean":
case "number":
case "string":
case "void":
default:
result = type.kind
}
let name = ""
// If the type has a name, it could be interesting to add it to the string
// representation of the type. However, when the type is a LiteralType, the
// name is already in result, so there's no need to add it.
if (type.kind != "LiteralType" && type.name) name = type.name + ": "
return name + result
}

View file

@ -381,6 +381,10 @@ export async function getProfileDir() {
}
}
if (config.get("profiledir") != "auto") {
return config.get("profiledir")
}
// First, see if we can get the profile from the arguments that were given
// to Firefox
let args = await ffargs()

View file

@ -10,7 +10,7 @@ function getChangelogDiv() {
function updateChangelogStatus() {
const changelogDiv = getChangelogDiv()
const changelogContent = changelogDiv.textContent
if (localStorage.changelogContent == changelogContent) {
if (localStorage["changelogContent"] == changelogContent) {
const changelogButton = document.querySelector('input[id^="spoiler"]')
if (!changelogButton) {
console.error("Couldn't find changelog button!")
@ -22,7 +22,7 @@ function updateChangelogStatus() {
function readChangelog() {
const changelogDiv = getChangelogDiv()
localStorage.changelogContent = changelogDiv.textContent
localStorage["changelogContent"] = changelogDiv.textContent
updateChangelogStatus()
}

View file

@ -226,9 +226,14 @@ a.url:hover {
padding-left: 0.5em;
width: 20%;
}
.SettingsCompletionOption td.title {
padding-left: 0.5em;
width: 20%;
}
.ExcmdCompletionOption td.type {
width: 30%;
}
.ExcmdCompletionOption td.documentation {
width: 50%;
width: 100%;
}

View file

@ -1,7 +1,6 @@
#cmdline_iframe {
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
z-index: 2147483647 !important;
width: 100% !important;
height: 0 !important;

View file

@ -77,6 +77,10 @@
padding-top: 7px !important;
}
#main-window[sizemode="maximized"] #content-deck {
padding-top: 8px;
}
/* Hide URL notifications that aren't particularly useful and cover up the command line
* TODO: move it above the command line / bring functionality into status line like Vimperator */
statuspanel[type="overLink"],

View file

@ -1,11 +1,10 @@
import { staticThemes } from "./.metadata.generated"
import * as config from "./config"
import * as Logging from "./logging"
const logger = new Logging.Logger("styling")
// find a way of getting theme list without hard-coding it
// using a macro might be an option
const THEMES = ["dark", "greenmat", "shydactyl", "quake"]
const THEMES = staticThemes
function capitalise(str) {
if (str === "") return str