mirror of
https://github.com/vale981/tridactyl
synced 2025-03-04 17:11:40 -05:00
Merge branch 'master' into feature/tab-groups
This commit is contained in:
commit
16f51c8513
124 changed files with 6793 additions and 5520 deletions
|
@ -3,3 +3,5 @@ build
|
|||
coverage
|
||||
*.test.ts
|
||||
test_utils.ts
|
||||
e2e_tests
|
||||
compiler
|
||||
|
|
21
.eslintrc.js
21
.eslintrc.js
|
@ -6,7 +6,7 @@ It represents the closest reasonable ESLint configuration to this
|
|||
project's original TSLint configuration.
|
||||
|
||||
We recommend eventually switching this configuration to extend from
|
||||
the recommended rulesets in typescript-eslint.
|
||||
the recommended rulesets in typescript-eslint.
|
||||
https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md
|
||||
|
||||
Happy linting! 💖
|
||||
|
@ -21,7 +21,6 @@ module.exports = {
|
|||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"prettier",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:sonarjs/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
|
@ -39,12 +38,7 @@ module.exports = {
|
|||
"sonarjs/no-duplicate-string": "off",
|
||||
"sonarjs/no-unused-collection": "off", //"error", // There seems to be a bug with this rule - exported collections are assumed unused
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
"@typescript-eslint/array-type": [
|
||||
"error",
|
||||
{
|
||||
"default": "array-simple"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/array-type": "off",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"@typescript-eslint/ban-types": [
|
||||
|
@ -75,7 +69,7 @@ module.exports = {
|
|||
"@typescript-eslint/class-name-casing": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/dot-notation": "error",
|
||||
"@typescript-eslint/dot-notation": "off", // this should be "error" but the fix silently breaks code almost 100% of the time. not worth the headaches
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"off",
|
||||
{
|
||||
|
@ -131,12 +125,13 @@ module.exports = {
|
|||
"allowTernary": true,
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"@typescript-eslint/no-unused-vars-experimental": [
|
||||
"error",
|
||||
{
|
||||
"args": "none",
|
||||
}
|
||||
"ignoreArgsIfArgsAfterAreUsed": true,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-as-const": "error",
|
||||
|
@ -223,7 +218,7 @@ module.exports = {
|
|||
"hoist": "all"
|
||||
}
|
||||
],
|
||||
"no-throw-literal": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-underscore-dangle": "off",
|
||||
|
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: glacambre
|
||||
github: bovine3dom
|
||||
patreon: tridactyl
|
||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY
|
||||
|
|
14
.github/workflows/e2e.yml
vendored
14
.github/workflows/e2e.yml
vendored
|
@ -49,28 +49,28 @@ jobs:
|
|||
run: |
|
||||
sudo yarn global add get-firefox
|
||||
get-firefox --platform linux --branch devedition --extract --target $HOME
|
||||
echo "::add-path::$HOME/firefox"
|
||||
echo "$HOME/firefox" >> $GITHUB_PATH
|
||||
- name: Install Firefox ESR (Linux)
|
||||
if: matrix.browser == 'firefoxesr' && matrix.os == 'ubuntu'
|
||||
run: |
|
||||
sudo yarn global add get-firefox
|
||||
get-firefox --platform linux --branch esr --extract --target $HOME
|
||||
echo "::add-path::$HOME/firefox"
|
||||
echo "$HOME/firefox" >> $GITHUB_PATH
|
||||
- name: Install Firefox Dev Edition (MacOS)
|
||||
if: matrix.browser == 'firefox' && matrix.os == 'macos'
|
||||
run: |
|
||||
brew cask --verbose --debug install homebrew/cask-versions/firefox-developer-edition
|
||||
echo "::add-path::/Applications/Firefox Developer Edition.app/Contents/MacOS/"
|
||||
brew install --cask homebrew/cask-versions/firefox-developer-edition
|
||||
echo "/Applications/Firefox Developer Edition.app/Contents/MacOS/" >> $GITHUB_PATH
|
||||
- name: Install Firefox Dev Edition (Windows)
|
||||
if: matrix.browser == 'firefox' && matrix.os == 'windows'
|
||||
run: |
|
||||
choco install firefox-dev --pre
|
||||
echo "::add-path::C:\Program Files\Firefox Dev Edition"
|
||||
echo "C:\Program Files\Firefox Dev Edition" >> $GITHUB_PATH
|
||||
- name: Install Firefox ESR (Windows)
|
||||
if: matrix.browser == 'firefoxesr' && matrix.os == 'windows'
|
||||
run: |
|
||||
choco install firefoxesr
|
||||
echo "::add-path::C:\Program Files\Mozilla Firefox"
|
||||
echo "C:\Program Files\Mozilla Firefox" >> $GITHUB_PATH
|
||||
|
||||
- name: Print Firefox version (Unix-like)
|
||||
if: matrix.os == 'ubuntu' || matrix.os == 'macos'
|
||||
|
@ -86,7 +86,7 @@ jobs:
|
|||
timeout_minutes: 10
|
||||
retry_wait_seconds: 10
|
||||
command: |
|
||||
yarn run clean && yarn run build && yarn make-zip && yarn jest
|
||||
yarn run clean && yarn run build --old-native && yarn make-zip && yarn jest
|
||||
# - name: Test (Chrome, Linux)
|
||||
# if: matrix.browser == 'chrome' && matrix.os == 'ubuntu'
|
||||
# run: xvfb-run --auto-servernum npm run jest -- ${{ matrix.browser }} || xvfb-run --auto-servernum npm run jest -- ${{ matrix.browser }}
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -14,3 +14,6 @@ compiler/*.js
|
|||
compiler/**/*.js
|
||||
.*.generated.ts
|
||||
.tmp/
|
||||
.DS_Store
|
||||
.build_cache/
|
||||
yarn-error.log
|
||||
|
|
328
.tridactylrc
328
.tridactylrc
|
@ -1,161 +1,173 @@
|
|||
" 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!
|
||||
" " bovine3dom's dogfood
|
||||
"
|
||||
" 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
|
||||
" ~/.config/tridactyl/tridactylrc to mere mortals) or ~/.tridactylrc and
|
||||
" 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
|
||||
" here makes the setting disappear, uncomment the line below.
|
||||
|
||||
"sanitise tridactyllocal tridactylsync
|
||||
|
||||
"
|
||||
" Binds
|
||||
"
|
||||
|
||||
" Comment toggler for Reddit, Hacker News and Lobste.rs
|
||||
bind ;c hint -Jc [class*="expand"],[class="togg"],[class="comment_folder"]
|
||||
|
||||
" 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")
|
||||
|
||||
|
||||
" make d take you to the tab you were just on (I find it much less confusing)
|
||||
bind d composite tab #; tabclose #
|
||||
bind D tabclose
|
||||
|
||||
" 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 yankcanon
|
||||
|
||||
" 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%20
|
||||
|
||||
"
|
||||
" Misc settings
|
||||
"
|
||||
|
||||
" set editorcmd to suckless terminal, or use the defaults on other platforms
|
||||
js tri.browserBg.runtime.getPlatformInfo().then(os=>{const editorcmd = os.os=="linux" ? "st vim" : "auto"; tri.config.set("editorcmd", editorcmd)})
|
||||
|
||||
" set profile dir on Windows
|
||||
jsb browser.runtime.getPlatformInfo().then(os=>{const profiledir = os.os=="win" ? "C:\\Users\\olie\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\gwm76nmk.default" : "auto"; tri.config.set("profiledir", profiledir)})
|
||||
|
||||
" Sane hinting mode
|
||||
set hintfiltermode vimperator-reflow
|
||||
set hintnames numeric
|
||||
|
||||
" 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
|
||||
|
||||
" Equivalent to `set csp clobber` before it was removed. This weakens your
|
||||
" defences against cross-site-scripting attacks and other types of
|
||||
" code-injection by reducing the strictness of Content Security Policy on
|
||||
" every site in a couple of ways.
|
||||
"
|
||||
" You may not wish to run this. Mozilla strongly feels that you shouldn't.
|
||||
"
|
||||
" It allows Tridactyl to function on more pages, e.g. raw GitHub pages.
|
||||
" " Every line in this file is commented out because in general they're all bad ideas.
|
||||
" " If anything in here was a good idea, it would be default behaviour in Tridactyl.
|
||||
" " It's just a collection of interesting ideas that happen to also be bovine3dom's
|
||||
" " RC file
|
||||
"
|
||||
" We remove the sandbox directive
|
||||
" https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
|
||||
" which allows our iframe to run (and anyone else's) on any website.
|
||||
"
|
||||
" We weaken the style-src directive
|
||||
" https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
|
||||
" to allow us to theme our elements. This exposes you to 'cross site styling'
|
||||
" attacks.
|
||||
"
|
||||
" Read https://wiki.mozilla.org/Security/CSP#Goals for more information.
|
||||
jsb browser.webRequest.onHeadersReceived.addListener(tri.request.clobberCSP,{urls:["<all_urls>"],types:["main_frame"]},["blocking","responseHeaders"])
|
||||
" " 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.
|
||||
"
|
||||
" " Uncomment the lines you want to keep then move this file to
|
||||
" " $XDG_CONFIG_DIR/tridactyl/tridactylrc (that's
|
||||
" " ~/.config/tridactyl/tridactylrc to mere mortals) or ~/.tridactylrc and
|
||||
" " install the native messenger (:installnative in Tridactyl). Run :source to
|
||||
" " get it in the browser, or just restart.
|
||||
"
|
||||
" " If you're bovine3dom run sed 's|^" ||' .tridactylrc > ~/.config/tridactyl/tridactylrc
|
||||
"
|
||||
"
|
||||
" "
|
||||
" " Binds
|
||||
" "
|
||||
"
|
||||
" " Comment toggler for Reddit, Hacker News and Lobste.rs
|
||||
" bind ;c hint -Jc [class*="expand"],[class="togg"],[class="comment_folder"]
|
||||
"
|
||||
" " 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")
|
||||
"
|
||||
"
|
||||
" " make d take you to the left (I find it much less confusing)
|
||||
" bind d composite tabprev; tabclose #
|
||||
" bind D tabclose
|
||||
"
|
||||
" " make t open the selection with tabopen
|
||||
" bind --mode=visual t composite js document.getSelection().toString() | fillcmdline tabopen
|
||||
"
|
||||
" " 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 #search div:not(.action-menu) > a
|
||||
" bindurl www.google.com F hint -Jbc #search div:not(.action-menu) > a
|
||||
"
|
||||
"
|
||||
" " DDG binds are broken as of May 2021
|
||||
" " 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
|
||||
"
|
||||
" " 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
|
||||
"
|
||||
" " Suspend / "discard" all tabs - handy for stretching out battery life
|
||||
" command discardall jsb browser.tabs.query({}).then(ts => browser.tabs.discard(ts.map(t=>t.id)))
|
||||
"
|
||||
" " Julia docs' built in search is bad
|
||||
" set searchurls.julia https://www.google.com/search?q=site:http://docs.julialang.org/en/v1%20
|
||||
"
|
||||
" "
|
||||
" " Misc settings
|
||||
" "
|
||||
"
|
||||
" " set editorcmd to suckless terminal, or use the defaults on other platforms
|
||||
" js tri.browserBg.runtime.getPlatformInfo().then(os=>{const editorcmd = os.os=="linux" ? "st vim" : "auto"; tri.config.set("editorcmd", editorcmd)})
|
||||
"
|
||||
" " set profile dir on Windows
|
||||
" jsb browser.runtime.getPlatformInfo().then(os=>{const profiledir = os.os=="win" ? "C:\\Users\\olie\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\gwm76nmk.default" : "auto"; tri.config.set("profiledir", profiledir)})
|
||||
"
|
||||
" " Sane hinting mode
|
||||
" set hintfiltermode vimperator-reflow
|
||||
" set hintnames numeric
|
||||
"
|
||||
" " 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
|
||||
"
|
||||
" " Equivalent to `set csp clobber` before it was removed. This weakens your
|
||||
" " defences against cross-site-scripting attacks and other types of
|
||||
" " code-injection by reducing the strictness of Content Security Policy on
|
||||
" " every site in a couple of ways.
|
||||
" "
|
||||
" " You may not wish to run this. Mozilla strongly feels that you shouldn't.
|
||||
" "
|
||||
" " It allows Tridactyl to function on more pages, e.g. raw GitHub pages.
|
||||
" "
|
||||
" " We remove the sandbox directive
|
||||
" " https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
|
||||
" " which allows our iframe to run (and anyone else's) on any website.
|
||||
" "
|
||||
" " We weaken the style-src directive
|
||||
" " https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
|
||||
" " to allow us to theme our elements. This exposes you to 'cross site styling'
|
||||
" " attacks.
|
||||
" "
|
||||
" " Read https://wiki.mozilla.org/Security/CSP#Goals for more information.
|
||||
" jsb browser.webRequest.onHeadersReceived.addListener(tri.request.clobberCSP,{urls:["<all_urls>"],types:["main_frame"]},["blocking","responseHeaders"])
|
||||
"
|
||||
" " Make quickmarks for the sane Tridactyl issue view
|
||||
" quickmark t https://github.com/tridactyl/tridactyl/issues?utf8=%E2%9C%93&q=sort%3Aupdated-desc+
|
||||
|
||||
" Make quickmarks for the sane Tridactyl issue view
|
||||
quickmark t https://github.com/tridactyl/tridactyl/issues?utf8=%E2%9C%93&q=sort%3Aupdated-desc+
|
||||
|
||||
" Inject Google Translate
|
||||
" This (clearly) is remotely hosted code. Google will be sent the whole
|
||||
" contents of the page you are on if you run `:translate`
|
||||
" From https://github.com/jeremiahlee/page-translator
|
||||
command translate js let googleTranslateCallback = document.createElement('script'); googleTranslateCallback.innerHTML = "function googleTranslateElementInit(){ new google.translate.TranslateElement(); }"; document.body.insertBefore(googleTranslateCallback, document.body.firstChild); let googleTranslateScript = document.createElement('script'); googleTranslateScript.charset="UTF-8"; googleTranslateScript.src = "https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit&tl=&sl=&hl="; document.body.insertBefore(googleTranslateScript, document.body.firstChild);
|
||||
|
||||
"
|
||||
" URL redirects
|
||||
"
|
||||
|
||||
" New reddit is bad
|
||||
autocmd DocStart ^http(s?)://www.reddit.com js tri.excmds.urlmodify("-t", "www", "old")
|
||||
" Mosquito nets won't make themselves
|
||||
autocmd DocStart ^http(s?)://www.amazon.co.uk js tri.excmds.urlmodify("-t", "www", "smile")
|
||||
|
||||
" Isolate Facebook in its own container
|
||||
" set auconcreatecontainer true
|
||||
" autocontain facebook\.com facebook
|
||||
|
||||
" For syntax highlighting see https://github.com/tridactyl/vim-tridactyl
|
||||
" vim: set filetype=tridactyl
|
||||
" " Quickmark for PRs by humans
|
||||
" quickmark p https://github.com/tridactyl/tridactyl/pulls?q=is%3Apr+is%3Aopen+-label%3Adependencies+-author%3Abovine3dom+sort%3Aupdated-desc
|
||||
"
|
||||
" " Inject Google Translate
|
||||
" " This (clearly) is remotely hosted code. Google will be sent the whole
|
||||
" " contents of the page you are on if you run `:translate`
|
||||
" " From https://github.com/jeremiahlee/page-translator
|
||||
" command translate js let googleTranslateCallback = document.createElement('script'); googleTranslateCallback.innerHTML = "function googleTranslateElementInit(){ new google.translate.TranslateElement(); }"; document.body.insertBefore(googleTranslateCallback, document.body.firstChild); let googleTranslateScript = document.createElement('script'); googleTranslateScript.charset="UTF-8"; googleTranslateScript.src = "https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit&tl=&sl=&hl="; document.body.insertBefore(googleTranslateScript, document.body.firstChild);
|
||||
"
|
||||
" "
|
||||
" " URL redirects
|
||||
" "
|
||||
"
|
||||
" " New reddit is bad
|
||||
" autocmd DocStart ^http(s?)://www.reddit.com js tri.excmds.urlmodify("-t", "www", "old")
|
||||
" " Mosquito nets won't make themselves
|
||||
" autocmd DocStart ^http(s?)://www.amazon.co.uk js tri.excmds.urlmodify("-t", "www", "smile")
|
||||
"
|
||||
" " Isolate Facebook in its own container
|
||||
" " set auconcreatecontainer true
|
||||
" " autocontain facebook\.com facebook
|
||||
"
|
||||
" " For syntax highlighting see https://github.com/tridactyl/vim-tridactyl
|
||||
" " vim: set filetype=tridactyl
|
||||
|
|
250
CHANGELOG.md
250
CHANGELOG.md
|
@ -1,5 +1,255 @@
|
|||
# Tridactyl changelog
|
||||
|
||||
# Release 1.22.0 / Unreleased
|
||||
|
||||
- New features
|
||||
|
||||
- A `superignore` setting has been added which nearly totally disables Tridactyl, per page. Use it with `:seturl` only. If you break Tridactyl with it, remember that you can always use the `tri` keyword in the address bar to execute ex-commands (#3497)
|
||||
|
||||
- `:drawing{start,stop,erasertoggle}` added to scribble on pages
|
||||
|
||||
- `:hint -V` hint mode added, bound to `;V`, which adds hints to all matching elements on the page, including ones outside the viewport (#3534)
|
||||
|
||||
- `:set completionfuzziness [0-1]` added to control strictness of completion matches (#3556)
|
||||
|
||||
- The cursor is no longer shown in no mouse mode (#3891)
|
||||
|
||||
- For our own `:find` mode, there is now a `findhighlighttimeout` setting (in milliseconds) which allows you to control how long text stays highlighted after a search (#3854)
|
||||
|
||||
- Tabs may now be renamed with `:tabrename` (#3577)
|
||||
|
||||
- Hint mode now tidies up any previous hints when it is entered
|
||||
|
||||
- `:colours midnight` added - a dark theme based on shydactyl (#3750)
|
||||
|
||||
- `:urlmodify` can now accept a URL as an argument (#3748)
|
||||
|
||||
- `:tabpush` now pushes to the next most recently used window if no other argument is given
|
||||
|
||||
- Bug fixes
|
||||
|
||||
- The new tab page no longer has invisible links on it when set to `about:blank` (#1743)
|
||||
|
||||
- `:viewconfig` might now work more reliably but we are not sure (#3653)
|
||||
|
||||
- it also works from Tridactyl pages now, at least some of the time
|
||||
|
||||
- `:nmode` is now shown in the mode indicator
|
||||
|
||||
- `:open` will now always run in its own tab (#3583)
|
||||
|
||||
- Hint modes now accept a much wider array of arguments (#3534)
|
||||
|
||||
- `:source --url` now works on data URIs (#3540)
|
||||
|
||||
- `:guiset` now gives a more useful error on reading a malformed userChrome.css (#3541)
|
||||
|
||||
- `:mpvsafe` now leaves MPV running after Firefox exits on all platforms (#3538)
|
||||
|
||||
- `:viewcontainers` uses `:jsonview` internally which should fix how it is displayed from Tridactyl pages (#3539)
|
||||
|
||||
- `:containerdelete` no longer closes all normal tabs if an invalid container is given (#3536)
|
||||
|
||||
- `:set auconcreatecontainer fales` is now respected (#3537)
|
||||
|
||||
- GitHub and other SVG favicons should appear in the tab completions (#3892)
|
||||
|
||||
- `:editor` now correctly returns a value (#3800)
|
||||
|
||||
- Miscellaneous
|
||||
|
||||
- We've switched from webpack to esbuild which has reduced dev-build times from 40 seconds to about 1 second (#3645)
|
||||
|
||||
- Tridactyl can now be built from ~5MB of source, down from ~100MB (#3632)
|
||||
|
||||
- We've switched from ramda to rambda which is a bit faster (#3628)
|
||||
|
||||
- We've switched from web-ext-types to definitely typed types, which are a bit better maintained
|
||||
|
||||
- We update the DOM in place a bit less, which could speed up `:tab` completions
|
||||
|
||||
- Hint mode is a bit faster (#3582)
|
||||
|
||||
- Dev builds of Tridactyl now open a new tab page when Tridactyl code is updated (#3564)
|
||||
|
||||
- We're now using `firenvim`s web-editor library which means that CodeMirror, Monaco and other in-browser code editors now work with `:editor` (#3851)
|
||||
|
||||
# Release 1.21.1 / 2021-03-19
|
||||
|
||||
- New features
|
||||
|
||||
- `:saveas` has new `--overwrite` and `--cleanup` flags ([#3362](https://github.com/tridactyl/tridactyl/issues/3362))
|
||||
- `:mousemode` added to revert `:nomousemode` ([#3442](https://github.com/tridactyl/tridactyl/issues/3442))
|
||||
- `:tabopen -w` added, which waits for the page to load before returning. Useful in `:composite` with e.g. `:composite tabopen -b -w news.bbc.co.uk ; tabnext` ([#3396](https://github.com/tridactyl/tridactyl/issues/3396))
|
||||
|
||||
- Bug fixes
|
||||
|
||||
- `:nativeinstall` now correctly bypasses execution policy on Windows
|
||||
- `gi` now supports "textbox" ARIA roles - i.e. it works on Twitter now ([#3459](https://github.com/tridactyl/tridactyl/issues/3459))
|
||||
- `w` in visual mode now selects the first character of the next word ([#3455](https://github.com/tridactyl/tridactyl/issues/3455))
|
||||
- Native messenger doesn't get stuck in an infinite loop on Windows for non-Firefox Firefox-derivate browsers (e.g. Waterfox) ([#3443](https://github.com/tridactyl/tridactyl/issues/3443))
|
||||
- `:native` now behaves better when reading non-existent files ([#3418](https://github.com/tridactyl/tridactyl/issues/3418))
|
||||
- `:nativeupdate` now works on Windows and elsewhere ([#3404](https://github.com/tridactyl/tridactyl/issues/3404))
|
||||
- NB: the success/failure messages are no longer meaningful; improving these again is a work in progress.
|
||||
- `text.backward_kill_word` command fixed on single characters ([#3405](https://github.com/tridactyl/tridactyl/issues/3405))
|
||||
|
||||
- Miscellaneous
|
||||
- CI linting now works ([#3477](https://github.com/tridactyl/tridactyl/issues/3477))
|
||||
|
||||
Thanks to all of our contributors for this release: dependabot-preview[bot], Oliver Blanthorn, Dhruva Sambrani, Rummskartoffel, Jez Cope, Babil G. Sarwar, Babil Golam Sarwar, Hosein Naghdbishi, Laura, William, WorldCodeCentral and fluem.
|
||||
|
||||
Extra special thanks go to Babil G. Sarwar, Dhruva Sambrani, Hosein Naghdbishi, Jez Cope, Laura and William who all contributed for the first time.
|
||||
|
||||
Last, but not least - thank you to everyone who reported issues.
|
||||
|
||||
# Release 1.21.0 / 2021-02-22
|
||||
|
||||
- New features
|
||||
|
||||
- `:tabclose` now accepts `window_number.tab_number` arguments; this is particularly useful for use with `<S-Del>` to close `:taball` completions
|
||||
- `:tab` completions now show which tabs are playing audio
|
||||
- `:tabaudio` tab with `ga` default bind takes you to the window and tab which is currently playing audio ([#3184](https://github.com/tridactyl/tridactyl/issues/3184))
|
||||
- Favicons are lazy loaded in `:tab` completions
|
||||
- `:urlincrement` binds now accept numeric prefixes, e.g. `10<C-a>` increments the URL by 10 ([#3145](https://github.com/tridactyl/tridactyl/issues/3145))
|
||||
- `:seturl` now checks that you have entered a valid RegEx ([#3134](https://github.com/tridactyl/tridactyl/issues/3134))
|
||||
- previously, failing to do so broke Tridactyl
|
||||
- `:colours` now accepts a `--url` option to load a theme from the internet with no need for `:native` ([#3148](https://github.com/tridactyl/tridactyl/issues/3148))
|
||||
- `:colours` now has completions for default and installed themes (i.e. themes for which you have already run `:colours` once)
|
||||
- `:colours` will fall back to loading custom themes from the Tridactyl storage if they cannot be found on disk
|
||||
- `yo` bind added to yank URLs and titles in an Emacs-compatible org-mode format
|
||||
- `:tab [string]` now switches to the first tab that matches that string ([#3263](https://github.com/tridactyl/tridactyl/issues/3263))
|
||||
- predominantly for non-interactive use. If you wish to use it interactively run `:set completions.Tab.autoselect false` first.
|
||||
- `:reloaddead` command added to force all tabs which Tridactyl is not running in to load. Useful for making tab switching more pleasurable ([#3260](https://github.com/tridactyl/tridactyl/issues/3260))
|
||||
- `:mkt --clipboard` added to put a Tridactyl RC file in your clipboard. Use with e.g. GitHub Gist and `:source --url` to avoid needing `:native` installed
|
||||
- Custom themes no longer require special classnames - see `:help colours` to see current requirements (there are essentially none) ([#3288](https://github.com/tridactyl/tridactyl/issues/3288))
|
||||
- `:set hintautoselect [true|false]` added to determine whether hints are automatically followed if there is only one ([#3097](https://github.com/tridactyl/tridactyl/issues/3097))
|
||||
- `:set logging.autocmds debug|info|warning|error` added to make debugging autocmds easier ([#3381](https://github.com/tridactyl/tridactyl/issues/3381))
|
||||
- `:set tabclosepinned true|false` added to prevent `d` from closing pinned tabs ([#3363](https://github.com/tridactyl/tridactyl/issues/3363))
|
||||
- `:tabsort` command added to sort tabs according to titles, URLs or container ([#3364](https://github.com/tridactyl/tridactyl/issues/3364))
|
||||
- `:winopen -c [container]` added for opening containers in new windows ([#3326](https://github.com/tridactyl/tridactyl/issues/3326))
|
||||
- `:set completions.TabAll.autoselect true|false` added to allow spaces to be used when filtering ([#1835](https://github.com/tridactyl/tridactyl/issues/1835))
|
||||
- `:scrollpage` binds now accept counts ([#3319](https://github.com/tridactyl/tridactyl/issues/3319))
|
||||
|
||||
- Bug fixes
|
||||
|
||||
- We now queue up commands that interact with completions alongside those completions ([#3196](https://github.com/tridactyl/tridactyl/issues/3196))
|
||||
- this means in practice that, once the command line has loaded, you can type as quickly as you like and still get the results you expect
|
||||
- Profile directory detection has been improved on Windows ([#3191](https://github.com/tridactyl/tridactyl/issues/3191))
|
||||
- Speed of `:editor` selection on Windows has improved ([#3170](https://github.com/tridactyl/tridactyl/issues/3170))
|
||||
- The command line no longer has a blue outline on OSX ([#3123](https://github.com/tridactyl/tridactyl/issues/3123))
|
||||
- `:yankimage` now accepts more MIME types ([#3127](https://github.com/tridactyl/tridactyl/issues/3127))
|
||||
- `:quickmarks` should now be a little more reliable ([#3299](https://github.com/tridactyl/tridactyl/issues/3299))
|
||||
- `:mkt` now no longer breaks with custom themes ([#2535](https://github.com/tridactyl/tridactyl/issues/2535))
|
||||
- `:firefoxsyncpush` no longer breaks with custom themes ([#3050](https://github.com/tridactyl/tridactyl/issues/3050))
|
||||
- `:bmark` completions no longer interfere with flags ([#3274](https://github.com/tridactyl/tridactyl/issues/3274))
|
||||
- `:zoom` works with negative relative increments ([#3031](https://github.com/tridactyl/tridactyl/issues/3031))
|
||||
- `:undo` completions now show negative times more gracefully ([#3339](https://github.com/tridactyl/tridactyl/issues/3339))
|
||||
- `:tabopen .thing` now searches for `.thing` ([#3398](https://github.com/tridactyl/tridactyl/issues/3398))
|
||||
- `:winopen` no longer puts focus in the URL bar
|
||||
|
||||
- Miscellaneous
|
||||
|
||||
- The native messenger is now written in `Nim` and is much faster (especially noticeable on `:editor`). You may need to run `:nativeupate` to update it - the latest `:native` version is `0.2.5`
|
||||
- We have removed `pyeval` support from this version - you are very unlikely to have used this since it was an internal Tridactyl command. `:exclaim` is unaffected.
|
||||
- `:taball` now internally uses `:tab` ([#3262](https://github.com/tridactyl/tridactyl/issues/3262))
|
||||
- We no longer load all default themes into every tab ([#3288](https://github.com/tridactyl/tridactyl/issues/3288))
|
||||
- `<C-c>` and `<C-a>` binds have been removed from the command line as they were widely disliked ([#3229](https://github.com/tridactyl/tridactyl/issues/3229))
|
||||
- `:colours shydactyl` now uses more CSS variables ([#3390](https://github.com/tridactyl/tridactyl/issues/3390))
|
||||
- Type checking has been tightened a little ([#3386](https://github.com/tridactyl/tridactyl/issues/3386))
|
||||
- Privacy policy added (summary: we don't collect anything outside of IP logs on our servers) ([#3375](https://github.com/tridactyl/tridactyl/issues/3375))
|
||||
- `:native` is now cached for a few milliseconds to speed up repeated version checks ([#3366](https://github.com/tridactyl/tridactyl/issues/3366))
|
||||
- `no-throw-literal` eslint rule added to ensure Tridactyl errors get to the user usefully
|
||||
|
||||
Thanks to all of our contributors for this release: dependabot-preview[bot], Oliver Blanthorn, Rummskartoffel, fluem, Benoit de Chezelles, Bruno Garcia, Jay Kamat, Babil Golam Sarwar, Elliott Shugerman, Annie Zhang, Tiago Epifânio, glacambre and yellowmoneybank.
|
||||
|
||||
Extra special thanks go to Annie Zhang, Benoit de Chezelles, Bruno Garcia, Elliott Shugerman, Jay Kamat, Tiago Epifânio and yellowmoneybank who all contributed for the first time.
|
||||
|
||||
Last, but not least - thank you to everyone who reported issues.
|
||||
|
||||
# Release 1.20.4 / 2020-12-21
|
||||
|
||||
- New features
|
||||
|
||||
- `;Y` image-to-clipboard hint mode ([#3085](https://github.com/tridactyl/tridactyl/issues/3085))
|
||||
- `:viewconfig` can now accept a dot-delimited path like `:set`, e.g. `:viewconfig completions.Tab`
|
||||
- `;x` and `;X` "emergency" hint modes added. They use `xdotool` and `:native` to move the mouse and click on the hinted element - if you don't have `xdotool` or `:native` installed they won't work. ([#3077](https://github.com/tridactyl/tridactyl/issues/3077))
|
||||
- Duplicates are now skipped in command history ([#3042](https://github.com/tridactyl/tridactyl/issues/3042))
|
||||
|
||||
- Bug fixes
|
||||
|
||||
- `:viewconfig` now gets completions and `:viewconfig --{user,default}` now accept a key to examine ([#3098](https://github.com/tridactyl/tridactyl/issues/3098))
|
||||
- `#` comments are now skipped in RC files ([#3100](https://github.com/tridactyl/tridactyl/issues/3100))
|
||||
- `:bind --mode=browser` now works with binds involving `Space` ([#3101](https://github.com/tridactyl/tridactyl/issues/3101))
|
||||
- `<C-Enter>` on the command line no longer inserts a space before its invocation ([#3089](https://github.com/tridactyl/tridactyl/issues/3089))
|
||||
- All "normal" clipboard operations now use the newer Clipboard API, fixing various bugs ([#3078](https://github.com/tridactyl/tridactyl/issues/3078))
|
||||
- web.whatsapp.com now has a special default hint mode so that hints actually work ([#1567](https://github.com/tridactyl/tridactyl/issues/1567))
|
||||
- `:nativeintall` can now run on POSIX-y systems without needing `bash` ([#3020](https://github.com/tridactyl/tridactyl/issues/3020))
|
||||
- `;#` hint mode now throws no errors if no anchors are found ([#2964](https://github.com/tridactyl/tridactyl/issues/2964))
|
||||
|
||||
- Miscellaneous
|
||||
|
||||
- Fix linter errors on src/excmds.ts by switching to `no-unused-vars-experimental` ([#3111](https://github.com/tridactyl/tridactyl/issues/3111))
|
||||
|
||||
Thanks to all of our contributors for this release: dependabot-preview[bot], Oliver Blanthorn, Rummskartoffel and Timothy Robert Bednarzyk.
|
||||
|
||||
Extra special thanks go to Timothy Robert Bednarzyk who contributed for the first time.
|
||||
|
||||
Last, but not least - thank you to everyone who reported issues.
|
||||
|
||||
# Release 1.20.3 / 2020-11-28
|
||||
|
||||
- New features
|
||||
|
||||
- `g!` jumbles all text on the page, inspired by [this letter](https://www.newscientist.com/letter/mg16221887-600-reibadailty/) ([#2913](https://github.com/tridactyl/tridactyl/issues/2913))
|
||||
- `:set modeindicatormodes.[mode] true|false` controls whether the mode indicator should show in a specific mode ([#2690](https://github.com/tridactyl/tridactyl/issues/2690))
|
||||
- New theme, `quakelight`, essentially identical to the default theme but with the command line at the top of the page.
|
||||
- Whether a completion autoselects the closest match is now configurable with `:set completions.[CompletionSource].autoselect true|false`. The completion sources are the ones Tridactyl uses internally - use `:get completions` to see the list ([#2901](https://github.com/tridactyl/tridactyl/issues/2901))
|
||||
- `:bmarks` now autoselects its completion by default. `:set completions.Bmark.autoselect false` to disable ([#2863](https://github.com/tridactyl/tridactyl/issues/2863))
|
||||
- `:undo tab_strict` only restores tabs in the current window ([#2883](https://github.com/tridactyl/tridactyl/issues/2883))
|
||||
- `:js` now accepts a flag, `-d`, to specify an EOF character which allows space-separated arguments to be given to it, stored in the array `JS_ARGS` ([#2859](https://github.com/tridactyl/tridactyl/issues/2859))
|
||||
- for example, `composite command only_second js -d% window.alert(JS_ARGS[1])%; only_second ignoreme SHOW_THIS! ignoreme ignoreme`
|
||||
- `UriChange` event has been added for `:autocmd`, for use on modern web applications which update their URI without navigating to a new page ([#3003](https://github.com/tridactyl/tridactyl/issues/3003))
|
||||
- this should only be used as a last resort as it uses a timer which can reduce battery life
|
||||
- `;K` hint mode added to reversibly hide elements from the page; hidden elements can be restored with `:elementunhide` ([#2934](https://github.com/tridactyl/tridactyl/issues/2934))
|
||||
|
||||
- Bug fixes
|
||||
|
||||
- `:undo` and `:rssexec` completions now autoselect the closest match, as was always intended ([#2901](https://github.com/tridactyl/tridactyl/issues/2901))
|
||||
- `:credits` no longer disappears before showing all authors ([#665](https://github.com/tridactyl/tridactyl/issues/665))
|
||||
- `:js -r` now works on Windows ([#3017](https://github.com/tridactyl/tridactyl/issues/3017))
|
||||
- `:hint` now can operate on `HTMLDetailsElements` ([#2984](https://github.com/tridactyl/tridactyl/issues/2984))
|
||||
- `:help` and `:tutor` now follow the Tridactyl theme ([#2895](https://github.com/tridactyl/tridactyl/issues/2895))
|
||||
|
||||
- Miscellaneous
|
||||
|
||||
- Various improvements to docs from a few different contributors
|
||||
- `nativeinstall` on Windows now installs the native messenger corresponding to your version of Tridactyl, meaning that we can finally make breaking changes to the native messenger! ([#3027](https://github.com/tridactyl/tridactyl/issues/3027))
|
||||
- `git hooks` no longer prevent committing from Windows ([#3033](https://github.com/tridactyl/tridactyl/issues/3033))
|
||||
|
||||
Thanks to all of our contributors for this release: dependabot-preview[bot], Oliver Blanthorn, fluem, Rummskartoffel, R Primus, Morgan Connolly, Sayan, Espen Henriksen, Mariusz Kaczmarczyk, glacambre and trixxo.
|
||||
|
||||
Extra special thanks go to Espen Henriksen, fluem, Mariusz Kaczmarczyk, R Primus, Rummskartoffel, Sayan and trixxo who all contributed for the first time.
|
||||
|
||||
Last, but not least - thank you to everyone who reported issues.
|
||||
|
||||
# Release 1.20.2 / 2020-09-27
|
||||
|
||||
- New features
|
||||
|
||||
- `:set escapehatchsidebarhack false` stops `<C-,>` from closing the sidebar (usually Tree Style Tab) at the expense of not being able to grab focus back from the address bar ([#2775](https://github.com/tridactyl/tridactyl/issues/2775))
|
||||
- `:autocmd` now provides magic variables for many events (so, e.g. you can tell an ex command which tab it should close). See `:help autocmd` and scroll down to the `...excmd` parameter for more information ([#2814](https://github.com/tridactyl/tridactyl/issues/2814))
|
||||
- `:zoom` now accepts a tab ID to tell it which tab to zoom ([#2809](https://github.com/tridactyl/tridactyl/issues/2809))
|
||||
|
||||
- Bug fixes
|
||||
- Normal mode now waits for user configuration to load before accepting any keypresses ([#2839](https://github.com/tridactyl/tridactyl/issues/2839))
|
||||
- Browser-wide maps now show up in `:bind` completions
|
||||
|
||||
Thanks to all of our contributors for this release: dependabot-preview[bot], Oliver Blanthorn and Simon H Moore
|
||||
|
||||
Extra special thanks go to Simon H Moore who contributed for the first time.
|
||||
|
||||
Last, but not least - thank you to everyone who reported issues.
|
||||
|
||||
# Release 1.20.1 / 2020-08-17
|
||||
|
||||
- Bug fixes
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#!/bin/sh
|
||||
cd ${0%/*}/..
|
||||
if ! [ -x "$(command -v shellcheck)" ]; then
|
||||
GLOBIGNORE="node_modules" shellcheck -e2012 **/*.sh
|
||||
cd "${0%/*}"/.. || exit 1
|
||||
if [ -x "$(command -v shellcheck)" ]; then
|
||||
GLOBIGNORE="node_modules" shellcheck -e2012 ./**/*.sh
|
||||
else
|
||||
echo "Warning: shellcheck is not installed, skipping shell scripts"
|
||||
fi
|
||||
yarn run lint
|
||||
"$(yarn bin)/eslint" --ext .ts
|
||||
"$(yarn bin)/eslint" --ext .ts .
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#! /bin/sh
|
||||
yarn run build --no-native
|
||||
cd ${0%/*}/../build
|
||||
cd "${0%/*}"/../build || exit 1
|
||||
"$(yarn bin)/web-ext" lint
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#! /bin/sh
|
||||
cd ${0%/*}
|
||||
cd "${0%/*}" || exit
|
||||
yarn run build --no-native
|
||||
"$(yarn bin)/jest" src
|
||||
|
|
|
@ -140,7 +140,7 @@ You can run Tridactyl easily in a temporary Firefox profile with `yarn run run`.
|
|||
|
||||
[Queensberry rules](https://en.oxforddictionaries.com/definition/queensberry_rules).
|
||||
|
||||
[matrix]: https://riot.im/app/#/room/#tridactyl:matrix.org
|
||||
[matrix]: https://matrix.to/#/#tridactyl:matrix.org
|
||||
[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
|
||||
|
|
59
doc/newsletters/05-autumn-2020.md
Normal file
59
doc/newsletters/05-autumn-2020.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
# Tridactyl news - Autumn 2020
|
||||
|
||||
Hello,
|
||||
|
||||
Welcome to the fifth quarterly Tridactyl newsletter.
|
||||
|
||||
We've had another productive quarter. It hasn't been quite as transformative as the last quarter - the addition of browser-wide `<C-,>` and `<C-6>` was hard to beat (I've personally switched to using `<C-,>` instead of `<Esc>` and generally Tridactyl and I get along a lot better :)) - but we have added quite a few nice little things.
|
||||
|
||||
## Highlighted new features
|
||||
|
||||
### For everyone
|
||||
|
||||
A few new features relating to completions have been added: `<C-o>yy` copies the currently selected completion to the clipboard, `<S-Delete>` executes `:tabclose` on a selected completion's arguments and `<C-Enter>` executes the highlighted completion keeping the command line open - particularly useful for, e.g. `tabopen -b`. The commands relating to those binds are `ex.execute_ex_on_completion{,_args} [ex command]`; the "args" variant means that the ex-command that triggered the completion is omitted from the final executed command.
|
||||
|
||||
Next, a quality of life improvement for some of our users. The mode indicator can now be hidden in individual modes with `:set modeindicatormodes.[mode] true|false`. I hope this is useful for e.g. full screen applications and ignore mode; users often were frustrated by the appearance of the mode indicator on YouTube.
|
||||
|
||||
My favourite feature of the quarter, from a contributor, is `:jumble`, bound to `g!` by default, which shuffles the characters in all text on the page while persrvineg the frist and last letetr of each word. Just for fun.
|
||||
|
||||
### For power users
|
||||
|
||||
I added a callback hintmode with `:hint -F elem => [... do some stuff in JS ...]`. I don't know why we didn't add it earlier - it was easy to implement and allows you to do a lot of cool stuff. It just goes to show that Tridactyl still has some low-hanging fruit!
|
||||
|
||||
autocommands now support `WebRequest` events - see `:help autocmd` for more details. It's especially useful for redirecting sites, like old reddit to new reddit, but comes with lots of caveats - rather than an ex-command, it runs a JavaScript snippet which must return an object with a specific format. There's no substitute for reading the help page on this one, I'm afraid.
|
||||
|
||||
`:js` now accepts a flag, `-d`, to specify an EOF character which allows space-separated arguments to be given to it, stored in the array `JS_ARGS`, meaning that people really can write their own ex-commands - the only thing left, really, is custom completions : ).
|
||||
|
||||
`:autocmd` now provides magic variables for lots of events - see `:help autocmd` to see them all. It's useful for ensuring that an ex-command affects the right tab even if that tab is opened in the background.
|
||||
|
||||
## Highlighted bug fixes
|
||||
|
||||
Tridactyl _finally_ should display the right version number on the new tab page and everywhere else, which means pretty version numbers for stable users. Beta users keep the current ugly version numbers : ).
|
||||
|
||||
In a fairly big change - you probably noticed it because your config disappeared despite my best efforts - we have ditched automatic synchronisation with the Firefox Sync storage. It had always caused problems for a minority of our users, but the introduction of a strict storage quota by Mozilla meant it broke for almost everyone using a custom theme. We now just use the local storage; if you want to keep your settings in sync with another machine, just run `:firefoxsync{pull,push}` periodically. Currently, the settings do not merge and will just overwrite when you push/pull - if anyone really wants that feature they should ask me. I suspect most people will manage fine without it. I'm hoping that this change might have made our RC files more reliable, but so far I haven't heard any noises either way.
|
||||
|
||||
We also fixed a bug that was particularly frustrating to new users: the little pop-up telling you the address of a link you are hovering over is now hidden when the command line is opened. We do this by putting focus on a fake empty link and then quickly deleting it - a trick we found in [VVimpulation](https://github.com/amedama41/vvimpulation), a smaller vim-like browser project that actually has quite a few neat little features. Open source is nice sometimes.
|
||||
|
||||
As a workaround to make `<C-,>` compatible with Tree Style Tab, `:set escapehatchsidebarhack` stops us from closing the sidebar. Unfortunately, that means that we can't get focus back from the address bar, so pick your poison.
|
||||
|
||||
Finally, a bug that I personally found very distracting has been squashed: `yy` should no longer give spurious errors.
|
||||
|
||||
## Other stuff + the near future
|
||||
|
||||
You might be interested to learn that [I did an interview for a little tech newsletter](https://console.substack.com/p/console-21), which was fun. Everyone loves talking about themselves.
|
||||
|
||||
In the next quarter, GitHub will stop doubling donations via GitHub sponsors. They still won't charge any fees, so it's still a vastly better deal than PayPal or Patreon (if your donation is small, those platforms will eat about ~30% of it by the time it gets to our pockets). This leaves us with a slight conundrum as my state-of-the-art forecasts predict that I would still like to eat broadly the same amount of food even after GitHub stops doubling the donations. I think we have enough users (and certainly the potential to have enough users) that, together, they could afford to keep me fed and maybe even housed [1]. I have a few ideas for persuading them to do this:
|
||||
|
||||
- porting to Chrome and charging a fairly high yearly licence fee for it + big updates to it (~tens of pounds); security updates and major bug/compatibility fixes we'd probably offer for free for as long as we could reasonably manage. Firefox would remain free and all our code would continue to be FOSS - people could just build Tridactyl for Chrome themselves if they wanted.
|
||||
|
||||
- offering a paid-for weekly newsletter with Tridactyl tips and tricks (~a few pounds a month). Not sure how I'd manage the mechanics of it - would people who got this newsletter currently be fed up if one suddenly turned up once a month?
|
||||
|
||||
- asking slightly more prominently in Tridactyl and explaining why we need donations in a little more detail. I'd probably add a setting to disable this ... I don't want to turn into Wikipedia (but Wikipedia is rolling in cash, so maybe I do?). We'd need the setting for the Chrome port, anyway, so that we weren't nagging people who had already paid.
|
||||
|
||||
In terms of features that are coming your way - I think I have finally been frustrated enough by our completion code that I am going to rewrite it to rely more heavily on configuration at runtime; I hope that will allow for more natural code re-use than the "you only have one parent" inheritance we currently use. It would also naturally allow users to write their own completions. I am optimistic that I will be able to merge the key-up binding PR in the next few months too - there's just one minor bug (that I know of) left to squash. The key-up bindings would allow for layers (e.g. hold \ to make j and k scroll farther) and "videogame style" smooth scrolling where the scrolling happens only while the key is held.
|
||||
|
||||
Thanks as ever for your support,
|
||||
|
||||
bovine3dom and the rest of the Tridactyl developers
|
||||
|
||||
[1]: NB - I am not in danger of starving or becoming homeless any time soon. However, there is a real risk that I would seek gainful employment if I was having to draw from my savings every month, which would mean less time spent on Tridactyl.
|
53
doc/newsletters/06-winter-2021.md
Normal file
53
doc/newsletters/06-winter-2021.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Tridactyl news - Winter 2021
|
||||
|
||||
Hello,
|
||||
|
||||
Welcome to the sixth quarterly Tridactyl newsletter. As a gentle reminder, the second (and first paywalled) Tridactyl top-tips newsletter will be going out in the next few days. If you'd like to receive it, simply up your monthly pledge to 10 USD or more on GitHub sponsors (preferred, as I get ~30%+ more of your money after taxes and transaction fees) or Patreon. I really appreciate every penny you send - it directly affects how long I can afford to work on Tridactyl. This next tips newsletter will be on making custom themes.
|
||||
|
||||
If you don't feel like you can afford the extra newsletter, don't worry about it too much. I find that when I start documenting how crazy parts of Tridactyl it is often easier to re-write how it works in Tridactyl rather than suffer the embarrassment of communicating it; so writing the tips newsletters benefits everyone. I'd also like to turn the newsletters into public wiki pages - I'll probably do that about 3 months after each one goes out.
|
||||
|
||||
## Highlighted new features
|
||||
|
||||
### Native messenger a bit quicker
|
||||
|
||||
The big change this quarter is that we've rewritten the native messenger - the little programme that lets Tridactyl interact with your computer, for example to read your files from your filesystem or restart Firefox - in Nim, a small compiled language previously called Nimrod. Compared to the previous Python version, Nim starts up much faster leading to around speed-ups of 2-100x depending on the command and your system. Windows users who were using the cross-compiled Python executables should notice a particularly large speedup; it is very obvious when using the editor on `<C-i>` in a text-box if you haven't manually set `:editorcmd`. At the time of writing this newsletter, the native messenger wasn't quite ready for release on OSX (a lack of testing) or Windows (reimplementing `:restart` is non-trivial). On Linux the messenger should update automatically in the latest betas if you already have native installed - just run `:native` to check. `0.2.0` is the first Nim version of the messenger. Use `:nativeupdate` to update it if it hasn't updated itself.
|
||||
|
||||
### Other stuff
|
||||
|
||||
Quite a few minor things have been added to Tridactyl, too. As always, I'll detail a few highlights here.
|
||||
|
||||
`:tabclose` now accepts `window_number.tab_number` arguments, meaning that Shift-Delete works to close tabs even in other windows on `:taball` completions. The selected completion on `:taball` doesn't currently move to the next option as you would expect. I'm hoping I'll get round to implementing that soon.
|
||||
|
||||
Three new hint modes were added: `;x` and `;X` "emergency" hint modes added if you have the native messenger and `xdotool` installed: these actually move the mouse and click on the element you select, so no amount of JS-shenanigans will break it. `;K` reversible element-"kill" hint mode added with `:elementunhide` to resurrect elements. `;Y` image-to-clipboard hint mode added.
|
||||
|
||||
Just for fun, `g!` jumbles all the text on the page, leaving the start and end of each word in the same place. Give your brain some exercise.
|
||||
|
||||
As a small quality-of-life improvement, we've changed how the clipboard commands (`yy`, etc) work under-the-bonnet to use a newer clipboard API. They're quite a bit quicker than they were and throw fewer errors now.
|
||||
|
||||
We've added a few treats for advanced users, too: a `UriChange` event added for `:autocmd` for use as a last resort on modern single-page application (SPA) sites where `DocLoad` events don't fire. Unfortunately it uses a little bit of extra power, so don't turn it on unless you need it. Additionally, `:js` accepts a flag `-d` and an EOF character, which will then give you `JS_ARGS` array to use of space-separated arguments, making it easier to make more complex ex-commands.
|
||||
|
||||
## Neat bug fixes
|
||||
|
||||
### Fewer race conditions on user input in the command line
|
||||
|
||||
In the command line, we now wait for completions to load before processing any commands that interact with the completions. In practice this means that if you have a website that you regularly visit, you can build up muscle memory to quickly go to and select that site - say, for example, you visit `news.bbc.co.uk` a lot, you can access it by typing `tne<Tab><Enter>`.
|
||||
|
||||
There's a caveat here in that you have to wait for the command line to have loaded at least a bit before you continue to type, but once the input box has appeared you can type as quickly as you like.
|
||||
|
||||
With this fix, I'm personally much happier using Tridactyl than I was. For me, it is a big step towards being a reliable tool I enjoy using and away from being a janky thing that I simply put up with.
|
||||
|
||||
### Other stuff
|
||||
|
||||
Help and tutor pages now follow your theme. `:editor` is quicker on Windows (even without the new native messenger) as we no longer check for Linux terminals which are vanishingly unlikely to exist. And that's pretty much it ... I think we must have added and then fixed lots of bugs in the beta which aren't really worth mentioning in these newsletters.
|
||||
|
||||
## Plans for next few months
|
||||
|
||||
I'll keep making the tips & tricks newsletters and maybe try to advertise them a little more. We currently have 12 donors across all platforms that will receive them. In monetary terms that is getting towards the "worth it" line, I think. We'll see whether it becomes too exhausting writing them each month, especially if I start tidying them up for the wiki too.
|
||||
|
||||
I'm still toying with the idea of looking seriously into a paid-for Chrome port, but I think I've found more things that I want to fix in Tridactyl first. I'm going to take another look at rewriting the completions code - updating the all-window-tab completions because they missed something that the one-window-tab completions had was unpleasant because there was so much duplicated code.
|
||||
|
||||
Finally, I expect I'll spend much of the next quarter fixing bugs I didn't spot in the new native messenger : )
|
||||
|
||||
As always, thanks for your generous support,
|
||||
|
||||
bovine3dom and the rest of the Tridactyl developers
|
45
doc/newsletters/07-spring-2021.md
Normal file
45
doc/newsletters/07-spring-2021.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Tridactyl news - Spring 2021
|
||||
|
||||
Hello,
|
||||
|
||||
Welcome to the seventh quarterly Tridactyl newsletter. Somehow I missed the quarter and it's now summer ... sorry! We added a few neat features this quarter - I find myself using `:tabaudio` a lot - but most of the work went on changes to our build system which have made working on Tridactyl vastly more pleasurable.
|
||||
|
||||
## Highlighted new features
|
||||
|
||||
We have a couple of new features for tab/window management. The first relates to tabs playing audio: `:tabaudio`, bound to `ga` by default takes you to the tab currently playing audio in your current window; and a little speaker is shown next to tabs playing audio on `:tab` completions. The second is that you can now open windows in specific containers by using `:winopen -c [container]`.
|
||||
|
||||
Also on completions, you can now `:set completionfuzziness [0-1]` to control how fuzzy completion searching is for lots of completions. If you find that you get surprising results when you're filtering completions, consider turning this setting down to get more exact matches.
|
||||
|
||||
Our hint mode argument parser was totally rewritten, meaning that essentially all flags which could meaningfully be combined now can be combined. At the same time, `:hint -V` was added for hinting the entire page rather than just the visible area.
|
||||
|
||||
I gave themes a bit of TLC in Tridactyl; you can now load themes directly from a URL with `:colours --url`; `:colours` now has completions; `:colours` will fallback to loading from Tridactyl storage if a theme isn't found on disk; and finally it is much easier to get starting writing themes as you no longer require any magic classnames. These improvements were inspired by the "Tips and Tricks" newsletter I was writing at the time (the one available for 10 USD sponsors/patrons and up) ... so I was right that they'd have trickle-down effects!
|
||||
|
||||
I finally fixed something a bit daft: you've been able to `:source --url` for a while to load an RC file from a web page without using the native messenger, but there was no way to write an RC file without the native messenger. Now you can, with `:mkt --clipboard` which will store your current settings as in an RC format in your system clipboard for you to paste wherever is convenient.
|
||||
|
||||
And finally, just for fun, a pull request added `:drawing{start,stop}`. These commands allow you to temporarily scribble on web pages with your mouse or stylus if you have one.
|
||||
|
||||
## Neat bug fixes
|
||||
|
||||
We've added a workaround for a Firefox bug that always confused new users: `:viewconfig` used to just give you a huge JSON dump on a single line when you called it from Tridactyl pages such as the new tab page or the help page, but it would appear in a snazzy JSON-viewer on other pages. It now works from Tridactyl tabs ... most of the time. There's a bug where sometimes it now just doesn't work at all, on any tab, if your system is overloaded. If that happens to you, close some tabs until we get around to fixing it.
|
||||
|
||||
We rewrote completions a little so that we update the DOM a bit less, which could improve performance, but it's hard to tell. I personally feel like `<S-Del>` on completions has sped up a bit but I've been wrong before.
|
||||
|
||||
## Stuff only Tridactyl developers care about
|
||||
|
||||
This newsletter is a bit shorter than previous ones - we've made fewer user-facing changes to Tridactyl this quarter. Instead, the bulk of my time on Tridactyl went on changing our build system from one centred on `webpack` to one centred on `esbuild`. Our development builds - how long it takes from making a change in Tridactyl to being able to test it - went from about 40 seconds to 1 second. It's made developing Tridactyl much less frustrating; it has made me very happy indeed. With luck this should mean that we can add features and fix bugs in Tridactyl more quickly.
|
||||
|
||||
At the same time, I rationalised the amount of stuff needed to reproduce a Tridactyl build - mostly because I felt sorry for Mozilla reviewers. Because our builds come with `:credits` containing an automatically generated list of contributors and `:version` which contains in some builds a git hash, you previously needed our entire git history to reproduce a build. Now we simply write those details to some small files when creating a build, so that a much smaller amount of code (5MB vs 100MB) is required. On a related note, `Tridactyl: Beta` builds got held up in the Mozilla review process for about 6 weeks, meaning no new releases were made in that time. It doesn't look like anyone other than me noticed. For now, our `Beta` releases will be semi-manual as Mozilla has started enforcing its "you must manually upload source code alongside your automated submissions" policy for non-listed extensions. This shouldn't make much difference to users but it has added a few minutes of drudgery to my week so I thought I would complain about it here...
|
||||
|
||||
## Plans for next few months
|
||||
|
||||
In the last newsletter I wrote that I thought I would probably spend a while fixing bugs in the native messenger, which came true (albeit with a lot of help from contributors). There are still a few - particularly, we broke `:set yankto selection`, which I keep meaning to look at.
|
||||
|
||||
Financially, donations are down a little this quarter, something along the lines of 20%, which I'll need to keep an eye on. I've been very slow with the newsletters - I'm not sure if that is a factor or if is just natural "leakage" as people rationalise their subscriptions. Anyway, I'll try to make sure the summer newsletter actually comes out in summer ... and as a gentle reminder, I get about 30% more of your money if you donate via GitHub Sponsors rather than Patreon.
|
||||
|
||||
You may also be aware that Mozilla is taking some steps towards implementing "Manifest V3", a Google initiative to "make web extensions more secure" (and accidentally nerf ad-blockers at the same time). Most of the noise online about that is about how Mozilla won't follow Google in making ad-blockers much worse, but they are intending to tighten up "content security policy" and use of `eval()` which _probably_ means that `:js`, `:jsb` and `:hint -F` callbacks will stop working [^1]. We could mitigate that by making `:composite` more powerful, but at some point if we make `:composite` too useful we will be violating the spirit of the rules. Switching to "Manifest V3" is planned to be compulsory some time in 2023, but as far as I understand it, Mozilla are talking about bringing some of these changes down to "Manifest V2" which we currently use, so we could see some features disappearing sooner. I'll keep you all up to date.
|
||||
|
||||
As always, thanks for your generous support,
|
||||
|
||||
bovine3dom and the rest of the Tridactyl developers
|
||||
|
||||
[^1]: a tiny angry footnote here: this is also going to stop in-browser language translation from working, which while already against Mozilla policies (no remote code execution) is currently possible. It infuriates me that Mozilla, while claiming to be a champion of minorities, doesn't see "allowing minorities to read the internet in their own language" as an absolute priority. Yes, Mozilla has their own offline language translation system in development but this is a bit like seeing a hungry person eating, slapping the food out of their hands because you think it is unhealthy and then planting a potato for them and expecting them to be grateful. `>:(`
|
27
doc/newsletters/08-summer-2021.md
Normal file
27
doc/newsletters/08-summer-2021.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Tridactyl news - Summer 2021
|
||||
|
||||
Hello,
|
||||
|
||||
Welcome to the eighth quarterly Tridactyl newsletter. This one's a bit shorter than usual as I had some consulting work and there's been less time since the previous newsletter. I'm trying to catch up and release the newsletters in the (northern hemisphere) seasons that they're named after.
|
||||
|
||||
## Highlighted new features
|
||||
|
||||
The big new feature this time is one that people have been asking for for a long time: Tridactyl now has a `superignore` setting that almost entirely disables Tridactyl after a page reload. It should be useful to web developers who want to make sure that Tridactyl is polluting their pages as little as possible while they are developing them.
|
||||
|
||||
You should only use it with `:seturl` for now - if you use it with `:set` it will be difficult to re-enable Tridactyl; the easiest thing to do then would be to use the `tri` keyword in the Firefox address bar to remove the setting with `:tri unset superignore`.
|
||||
|
||||
And that's it, really - there're a few other very minor new things (a `:tabrename` command, a new `midnight` theme, and `:no_mouse_mode` now hides the mouse, for example).
|
||||
|
||||
## Neat bug fixes and stuff only Tridactyl developers care about
|
||||
|
||||
`:editor` should now work with lots more web-based text editors as we're now using Firenvim's code, which glacambre kindly extracted out into a library here: https://github.com/glacambre/editor-adapter. We'll now share bug fixes and be able to collaborate on supporting new editors, which is pretty exciting to me.
|
||||
|
||||
We also had a tiny fix for a bug that's been getting on my nerves for a while: SVG favicons on `:tab` completions. This means you'll correctly see, amongst others, the GitHub favicon in that list.
|
||||
|
||||
## Plans for next few months
|
||||
|
||||
I might actually rewrite completions this quarter! I've been threatening to do it for years but a few days ago I got fed up enough with the code that I started sketching out a replacement here: https://github.com/tridactyl/tridactyl/issues/3896. If you have any thoughts on what you'd like our completions / command line to be like, please do share them in that issue. My main aims are to reduce code duplication, improve responsiveness and allow users to write their own completions at runtime.
|
||||
|
||||
As always, thanks for your generous support,
|
||||
|
||||
bovine3dom and the rest of the Tridactyl developers
|
152
doc/newsletters/tips-and-tricks/1-hint-css-selectors.md
Normal file
152
doc/newsletters/tips-and-tricks/1-hint-css-selectors.md
Normal file
|
@ -0,0 +1,152 @@
|
|||
# Tridactyl Tips & Tricks 1: Hint mode CSS selectors
|
||||
|
||||
Hi!
|
||||
|
||||
Welcome to the first Tridactyl Tips & Tricks newsletter, as mentioned in the previous Tridactyl newsletter. This newsletter is very experimental so any feedback you have would be appreciated.
|
||||
|
||||
This first edition is going out to all sponsors on GitHub and Patreon. Later editions will only go out to sponsors on tiers **10 USD a month and higher**; I'm trying to raise a bit more revenue since GitHub will no longer double donations. I'll probably make each newsletter public after a couple of months as I don't like restricting knowledge needlessly, but I want there to be some incentive other than warm fuzzy feelings for people to donate money to Tridactyl. My initial plan is to write a chunky guide like this about once a month - planned topics include custom ex-commands with `:js` and `:jsb`, `:composite` and custom themes - and once I run out of big ideas I'll send out shorter emails with more random tips & tricks more often.
|
||||
|
||||
In my experience, most people who use Tridactyl know a lot about computers in general, but don't know much about web technologies. These guides therefore will assume very little knowledge about JavaScript or the working of websites. They may assume some rudimentary knowledge of programming terminology. Please do let me know if I'm getting the balance right!
|
||||
|
||||
I wasn't sure where to start with the tips so I've gone for one of the features of Tridactyl I use most frequently: custom hint modes that use custom CSS selectors to only show the most relevant hints on your favourite websites; for example, on a search engine you might only want search results to be hinted. Essentially, we'll learn how to create lines like the following ones from my RC file.
|
||||
|
||||
```
|
||||
" Only hint search results on Google and DDG
|
||||
bindurl www.google.com f hint -Jc .rc > div > a
|
||||
bindurl www.google.com F hint -Jbc .rc > div > a
|
||||
|
||||
bindurl ^https://duckduckgo.com f hint -Jc [class=result__a]
|
||||
bindurl ^https://duckduckgo.com F hint -Jbc [class=result__a]
|
||||
|
||||
|
||||
" Comment toggler for Reddit, Hacker News and Lobste.rs
|
||||
bind ;c hint -Jc [class*="expand"],[class="togg"],[class="comment_folder"]
|
||||
```
|
||||
|
||||

|
||||
|
||||
We'll cover quite a lot of ground here so bear with me:
|
||||
|
||||
1. What a CSS selector does and why it's useful for hint modes
|
||||
2. How to craft a CSS selector that only contains the links you want
|
||||
3. Using this CSS selector in various hint modes
|
||||
4. Binding these hint modes to keys
|
||||
5. Binding these hint modes to keys only on certain websites
|
||||
|
||||
If you already know how to do any of those steps you can just skip the corresponding section.
|
||||
|
||||
Without further ado:
|
||||
|
||||
# 1: Introduction to CSS selectors and why they're useful for hint modes
|
||||
|
||||
CSS - "cascading style sheets" - control how a web page is displayed. Here is a simple CSS snippet:
|
||||
|
||||
```css
|
||||
p {
|
||||
color: pink;
|
||||
}
|
||||
```
|
||||
|
||||
This file would make all the paragraphs (the `<p>` tags) on the page pink.
|
||||
|
||||
Why do we care about this? The bit before the curly bracket - `p` - is a CSS selector. It tells the browser which parts of the page to apply the following styles to. Tridactyl can use this same technology to pick which elements of a page to hint with the syntax `:hint -c [CSS selector]`. For reasons of backwards compatibility, this also includes hints for any elements for which the page is listening for mouse click events with JavaScript; you can turn this off with the `J` flag so in practice you will usually see this invoked as `:hint -Jc [CSS selector]`. Selectors can be combined with some but not all hint modes - we will cover them in section 3.
|
||||
|
||||
MDN has an excellent tutorial on CSS selectors here: https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors . I highly recommend that you take the time to go through it.
|
||||
|
||||
# 2: How to craft a CSS selector that only contains the elements you want
|
||||
|
||||
In this section we'll make use of the Firefox web console to help us to find an initial CSS selector that selects the elements we want to hint and then improve it.
|
||||
|
||||
If you skipped the MDN tutorial above, you can get by in this section by knowing that websites generally denote "types" of element with classes, e.g. `<p class="advertorial">` might denote text that has been written by a sponsor. CSS selectors select classes with a leading dot, e.g. `.advertorial`. Tags are selected simply with the name of the tag and can be combined with other selectors, e.g. `p.advertorial`. The next important thing to know is that you can select direct children of other selectors with the child operator, `>`. So, if there were any `<a>` tags (i.e. links) within the advertorial, we can select them with `p.advertorial>a`. In sum, then, a hint mode for clicking on links within advertorials would be `:hint -Jc p.advertorial>a`.
|
||||
|
||||
With that out of the way, let's look at how to use the Firefox developer tools to craft the best CSS selector for your elements. For this example, I'll pretend that I want to hint the main articles on the [English Wikipedia homepage](https://en.wikipedia.org/wiki/Main_Page). Right click on one of the elements you want to hint and click "Inspect Element". A panel should appear; you want to look at the HTML panel and see if there is any discernible pattern to the tags surrounding the element you are interested in. Right click another element you want to hint and check that it is the same. For example, for me, the HTML looks a bit like this:
|
||||
|
||||
```
|
||||
<p>
|
||||
<b>
|
||||
<a href="/wiki/James_Humphreys_(pornographer)" title="James Humphreys (pornographer)">James Humphreys</a>
|
||||
</b>
|
||||
...
|
||||
<li>
|
||||
::marker
|
||||
<b>
|
||||
<a href="/wiki/Hurricane_Iota" title="Hurricane Iota">Hurricane Iota</a>
|
||||
</b>
|
||||
...
|
||||
```
|
||||
|
||||

|
||||
|
||||
It looks like the links I want to hint are always directly enclosed by a `<b>` (bold) tag. The CSS selector we want is therefore `b>a`, that is links (`a` tags) which are immediate children of bold tags. We can check that this works in Tridactyl by giving focus to the webpage again, and typing `:hint -c b>a` - and, if it works, we're done!
|
||||
|
||||

|
||||
|
||||
However, what if it didn't work? What if there were too many links hinted or not enough? I have found that the best way to proceed is to go to the Firefox console. On the panel that displays the HTML, click the "console" tab. In this tab, type
|
||||
|
||||
```
|
||||
document.querySelectorAll("[your selector here]")
|
||||
```
|
||||
|
||||
and press enter. So, for me, `document.querySelectorAll("b>a")`. You should see that a `NodeList` has been returned. If you click the little arrow/triangle next to this, it will expand and you can see all the elements that your selector matches (including ones not visible in the viewport).
|
||||
|
||||
If your selector has not matched the elements you want - and you can see which is which easily by hovering over them with the mouse and they will be highlighted in the viewport - you need to return to the previous step and make your CSS selector less restrictive. For example, we might change our selector from `b>a` to `a` - removing the restriction that `a` tags have to be inside `b` tags and instead selecting all `a` tags.
|
||||
|
||||
If instead your selector has matched too many elements, we need to tighten our CSS selector. If you can see a pattern that the elements you want follow but the elements that you don't want do not, use it. Let's say that on the Wikipedia page I don't want to hint any of the important links that go to external pages.
|
||||
|
||||
My NodeList looks like this:
|
||||
|
||||
```
|
||||
NodeList(57)
|
||||
0: <a class="" href="/wiki/James_Humphreys_(pornographer)" title="James Humphreys (pornographer)">
|
||||
1: <a class="" href="/wiki/James_Humphreys_(pornographer)" title="James Humphreys (pornographer)">
|
||||
...
|
||||
55: <a class="external text" href="https://en.wikivoyage.org/">
|
||||
56: <a class="external text" href="https://en.wiktionary.org/">
|
||||
```
|
||||
|
||||

|
||||
|
||||
where the first two links are ones I want and the last four are ones I want to exclude.
|
||||
|
||||
There are two patterns that I can spot: the links we want always have their hrefs start with `/wiki/`, and the links we don't want have `class="external text"`.
|
||||
|
||||
At this point it's helpful to have a working knowledge of the more advanced CSS selectors. Particularly, we will use [attribute selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors).
|
||||
|
||||
Since there are two patterns which completely specify the items we want and don't want, there are two possible CSS selectors. The first is relatively straightforward: `b>a[href^='/wiki/']`, meaning any `a` tags whose hrefs start with "/wiki/" and are the immediate children of `b` tags. The second is a little more complex as it requires a negation: `b>a:not([class^="external"])` - it matches all `a` tags whose classes don't start with "external" and are immediate children of `b` tags. We can verify that these work again with `:hint -c [CSS selector]` in Tridactyl or `document.querySelectorAll("[CSS selector]")` in the Firefox console.
|
||||
|
||||
For the special case where there is a single link that you would like to hint, you can use Tridactyl to create a unique selector automatically. Simply run `hint -F e => tri.excmds.fillcmdline("hint -Jc " + tri.dom.getSelector(e))` and Tridactyl will return a hint command for you to run or edit.
|
||||
|
||||
Finally, we will cover what to do if there appears to be no difference between the elements you want and the elements don't want in your `NodeList`: you need to look at the tags surrounding the elements. To do that, you can click on the "crosshair" like icon to the right of each element. This will take you back to the HTML representation of the page with your element selected. You then need to repeat the step we did initially, looking to see if you can spot any patterns. You can click the "console" tab to get back to the `NodeList` and look at other elements. If you're still stuck after all this, feel free to ask us on Matrix; there's usually a way to do it.
|
||||
|
||||
# 3. Using CSS selectors in more hint modes
|
||||
|
||||
There are a couple of gotchas to using anything other than the standard `:hint -c` hint mode. It can currently only be used on the default, foreground tab (`-t`), background tab (`-b`) and custom callback (`-F`) hintmodes.
|
||||
|
||||
The main caveat is that you can't put any spaces in the CSS selector if your mode also takes other arguments. E.g. `:hint -Fc b > a console.log` will not work: you need `:hint -Fc b>a console.log`. This means that CSS selectors that require spaces can't be used with these hint modes unless you start the hint mode via `:js` (which will be covered in a later newsletter ; )). If the mode takes other arguments, the selector always comes first.
|
||||
|
||||
# 4. Binding the hint mode to keys
|
||||
|
||||
I imagine most people reading this newsletter will already know how to bind to keys. I include it here for completeness.
|
||||
|
||||
`:bind [key sequence] hint -c [CSS selector]`, so for our Wikipedia example, we might choose `:bind ,f hint -c b>a`. See `:help bind` in Tridactyl for more information on key sequences.
|
||||
|
||||
# 5. Binding hint modes to keys only on certain websites
|
||||
|
||||
Custom hint modes like the ones we have covered here are usually specific to a single website. It therefore often makes sense to only bind the modes to keys when you are on these websites. Tridactyl has a `:bindurl` command for situations exactly like these.
|
||||
|
||||
The full syntax is `:bindurl [URL regex] [key sequence] [ex-command]`. If you are unfamiliar with JavaScript regex, you may find [this MDN page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) useful. Note that `:bindurl` converts the regex string to a regex, i.e. you should not surround your regex with slashes, so `\.xml$` rather than `/\.xml$/`. However, most of the time you don't really need to worry about bindurl using a regex. `:bindurl www.google.com ,c tabclose` will match "www.google.com" fine, but it will also match any URL that contains "www.google.com" and the dots `.` can actually match any letter, e.g. "wwwagooglezcom" would also match. In the real world this doesn't matter much, but if you are concerned about it you can use `^http(s)?://www\.google\.com` which will only match literal dots and only URLs that start with `google.com`.
|
||||
|
||||
So, for our Wikipedia example, we might use
|
||||
|
||||
```
|
||||
bindurl http(s)?://en\.wikipedia\.org/wiki/Main_Page ,f hint -c b>a`
|
||||
bindurl http(s)?://en\.wikipedia\.org/wiki/Main_Page ,F hint -bc b>a`
|
||||
```
|
||||
|
||||
where we have bound the new modes to `,f` and `,F`. If we wanted to "replace" the normal hint modes on these pages, we would just bind to `f` and `F` instead.
|
||||
|
||||
# Conclusion
|
||||
|
||||
I hope you have enjoyed this first tips & tricks newsletter. Please do send me feedback - whether it's via Matrix, GitHub issues or email - so I can get an idea on how useful this was. Was it too long? Was the topic interesting? Was it too easy, or too hard? How frequently do you think they should be sent?
|
||||
|
||||
Cheers, bovine3dom
|
|
@ -16,7 +16,7 @@ If you're having trouble running your editor on OSX, you might be having \$PATH
|
|||
|
||||
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.
|
||||
If you're on Unix, running `printf '%c\0\0\0{"cmd": "run", "command": "echo $PATH"}' 39 | ~/.local/share/tridactyl/native_main` in a terminal after you have installed the native messenger will check that it is at least partially working.
|
||||
|
||||
# Getting logging information
|
||||
|
||||
|
@ -35,13 +35,13 @@ In order to activate logging for a component, you can use the following command:
|
|||
|
||||
This will open a two consoles where Tridactyl's messages are logged. Click on the little bin icons in the consoles in order to remove previous messages and try to re-trigger the bug. Copy the logs as you would any other text, and then paste them in your GitHub issue in a block surrounded by three backticks like so:
|
||||
|
||||
````
|
||||
```
|
||||
\`\`\`
|
||||
logs
|
||||
go
|
||||
here
|
||||
\`\`\`
|
||||
```
|
||||
````
|
||||
|
||||
Unfortunately, Firefox truncates some objects, so if there are any that look particularly important, please copy them manually by right clicking on them and clicking "Copy object".
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ const modToSelenium = {
|
|||
}
|
||||
|
||||
export function sendKeys (driver, keys) {
|
||||
const delay = 300
|
||||
const delay = 500
|
||||
function chainRegularKeys (previousPromise, regularKeys) {
|
||||
return regularKeys
|
||||
.split("")
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
# stolen from https://gist.github.com/sindresorhus/7996717
|
||||
|
||||
echo "Running post-checkout hook..."
|
||||
|
||||
changed_files="$(git diff-tree -r --name-only --no-commit-id $1 $2)"
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
# stolen from https://gist.github.com/sindresorhus/7996717
|
||||
|
||||
echo "Running post-merge hook..."
|
||||
|
||||
changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
case $(uname) in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
echo "Cygwin/MinGW/MSYS detected, skipping pre-commit hook"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Running pre-commit hook..."
|
||||
|
||||
source ./scripts/common.sh
|
||||
|
||||
jsfiles=$(cachedTSLintFiles)
|
||||
|
|
|
@ -8,27 +8,27 @@ 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/tridactyl/tridactyl/tree/master/doc/troubleshooting.md] first.
|
||||
Please read and follow 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:
|
||||
|
||||
-->
|
||||
|
||||
* Brief description of the problem:
|
||||
- Brief description of the problem:
|
||||
|
||||
* Steps to reproduce:
|
||||
- Steps to reproduce:
|
||||
|
||||
1. 2. 3. 4. 5.
|
||||
|
||||
* Tridactyl version (`:version`):
|
||||
- Tridactyl version (`:version`):
|
||||
|
||||
* Firefox version (Top right menu > Help > About Firefox):
|
||||
- Firefox version (Top right menu > Help > About Firefox):
|
||||
|
||||
* URL of the website the bug happens on:
|
||||
- URL of the website the bug happens on:
|
||||
|
||||
* Config (in a new tab, run `:viewconfig`, copy the url and paste it somewhere like gist.github.com):
|
||||
- Config (in a new tab, run `:viewconfig --user`, copy the url and paste it somewhere like gist.github.com):
|
||||
|
||||
* Contents of ~/.tridactylrc or ~/.config/tridactyl/tridactylrc (if they exist):
|
||||
- Contents of ~/.tridactylrc or ~/.config/tridactyl/tridactylrc (if they exist):
|
||||
|
||||
```
|
||||
Insert tridactylrc contents between the backticks
|
||||
|
@ -36,6 +36,8 @@ Insert tridactylrc contents between the backticks
|
|||
|
||||
<!-- If your bug is about Tridactyl's native executable, please add the following information: -->
|
||||
|
||||
* Operating system:
|
||||
* Result of running `:! echo $PATH`, or `! echo %PATH%` on Windows, in Tridactyl:
|
||||
* Unix-like only: result of running `printf '%c\0\0\0{"cmd": "run", "command": "echo $PATH"}' 39 | ~/.local/share/tridactyl/native_main.py` in a terminal:
|
||||
- Operating system:
|
||||
- Result of running `:! echo $PATH`, or `! echo %PATH%` on Windows, in Tridactyl:
|
||||
- Unix-like only:
|
||||
- `:native` less than 0.2.0: result of running `printf '%c\0\0\0{"cmd": "run", "command": "echo $PATH"}' 39 | ~/.local/share/tridactyl/native_main.py` in a terminal:
|
||||
- `:native` at least version 0.2.0: result of running `printf '%c\0\0\0{"cmd": "run", "command": "echo $PATH"}' 39 | ~/.local/share/tridactyl/native_main` in a terminal:
|
||||
|
|
|
@ -13,7 +13,7 @@ module.exports = {
|
|||
"ts-jest": {
|
||||
tsConfig: {
|
||||
...tsConfig.compilerOptions,
|
||||
types: ["jest", "node", "web-ext-types"]
|
||||
types: ["jest", "node", "@types/firefox-webext-browser"]
|
||||
},
|
||||
diagnostics: {
|
||||
ignoreCodes: [151001]
|
||||
|
|
5
native/README.md
Normal file
5
native/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Deprecated
|
||||
|
||||
These files, except for `current_native_version`, are all deprecated. They are kept here for backwards compatibility and for people who cannot install the current native binary due to corporate IT security policies.
|
||||
|
||||
The current native messenger may be found here: https://github.com/tridactyl/native_messenger
|
1
native/current_native_version
Normal file
1
native/current_native_version
Normal file
|
@ -0,0 +1 @@
|
|||
0.3.6
|
|
@ -1,85 +1,99 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
#!/usr/bin/env sh
|
||||
|
||||
echoerr() {
|
||||
red="\\033[31m"
|
||||
normal="\\e[0m"
|
||||
echo -e "$red$*$normal" >&2
|
||||
printf "%b\n" "$red$*$normal" >&2
|
||||
}
|
||||
|
||||
sedEscape() {
|
||||
sed 's/[&/\]/\\&/g' <<< "$@"
|
||||
printf "%s" "$@" | sed 's/[&/\]/\\&/g'
|
||||
}
|
||||
|
||||
trap "echoerr 'Failed to install!'" ERR
|
||||
# To install, curl -fsSl 'url to this script' | sh
|
||||
|
||||
# To install, curl -fsSl 'url to this script' | bash
|
||||
run() {
|
||||
set -e
|
||||
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/tridactyl"
|
||||
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/tridactyl"
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/tridactyl"
|
||||
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/tridactyl"
|
||||
|
||||
# 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"
|
||||
# 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
|
||||
linux-gnu|linux|freebsd*)
|
||||
manifest_home="$HOME/.mozilla/native-messaging-hosts/"
|
||||
;;
|
||||
darwin*)
|
||||
manifest_home="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts/"
|
||||
;;
|
||||
*)
|
||||
# Fallback to default Linux location for unknown OSTYPE
|
||||
manifest_home="$HOME/.mozilla/native-messaging-hosts/"
|
||||
;;
|
||||
esac
|
||||
# Decide where to put the manifest based on OS
|
||||
# Get OSTYPE from bash if it's installed. If it's not, then this will
|
||||
# default to the Linux location as OSTYPE will be empty
|
||||
OSTYPE="$(command -v bash >/dev/null && bash -c 'echo $OSTYPE')"
|
||||
case "$OSTYPE" in
|
||||
linux-gnu|linux-musl|linux|freebsd*)
|
||||
manifest_home="$HOME/.mozilla/native-messaging-hosts/"
|
||||
;;
|
||||
darwin*)
|
||||
manifest_home="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts/"
|
||||
;;
|
||||
*)
|
||||
# Fallback to default Linux location for unknown OSTYPE
|
||||
manifest_home="$HOME/.mozilla/native-messaging-hosts/"
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$manifest_home" "$XDG_DATA_HOME"
|
||||
mkdir -p "$manifest_home" "$XDG_DATA_HOME"
|
||||
|
||||
manifest_file="$manifest_home/tridactyl.json"
|
||||
native_file="$XDG_DATA_HOME/native_main.py.new"
|
||||
native_file_final="$XDG_DATA_HOME/native_main.py"
|
||||
manifest_file="$manifest_home/tridactyl.json"
|
||||
native_file="$XDG_DATA_HOME/native_main.py.new"
|
||||
native_file_final="$XDG_DATA_HOME/native_main.py"
|
||||
|
||||
echo "Installing manifest here: $manifest_home"
|
||||
echo "Installing script here: XDG_DATA_HOME: $XDG_DATA_HOME"
|
||||
echo "Installing manifest here: $manifest_home"
|
||||
echo "Installing script here: XDG_DATA_HOME: $XDG_DATA_HOME"
|
||||
|
||||
# Until this PR is merged into master, we'll be copying the local version over
|
||||
# instead of downloading it
|
||||
if [[ "$1" == "local" ]]; then
|
||||
cp -f native/tridactyl.json "$manifest_file"
|
||||
cp -f native/native_main.py "$native_file"
|
||||
# Until this PR is merged into master, we'll be copying the local version
|
||||
# over instead of downloading it
|
||||
if [ "$1" = "local" ]; then
|
||||
cp -f native/tridactyl.json "$manifest_file"
|
||||
cp -f native/native_main.py "$native_file"
|
||||
else
|
||||
curl -sS --create-dirs -o "$manifest_file" "$manifest_loc"
|
||||
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"
|
||||
|
||||
# Requirements for native messenger
|
||||
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"
|
||||
else
|
||||
echoerr "Error: Python 3 must exist in PATH."
|
||||
echoerr "Please install it and run this script again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Successfully installed Tridactyl native messenger!"
|
||||
echo "Run ':native' in Firefox to check."
|
||||
}
|
||||
|
||||
# Run the run function in a subshell so that it can be exited early if an error
|
||||
# occurs
|
||||
if ret="$(run "$@")"; then
|
||||
# Print captured output
|
||||
printf "%b\n" "$ret"
|
||||
else
|
||||
curl -sS --create-dirs -o "$manifest_file" "$manifest_loc"
|
||||
curl -sS --create-dirs -o "$native_file" "$native_loc"
|
||||
# Print captured output, ${ret:+\n} adds a newline only if ret isn't empty
|
||||
printf "%b" "$ret${ret:+\n}"
|
||||
echoerr 'Failed to install!'
|
||||
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"
|
||||
|
||||
# Requirements for native messenger
|
||||
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"
|
||||
else
|
||||
echoerr "Error: Python 3 must exist in PATH."
|
||||
echoerr "Please install it and run this script again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Successfully installed Tridactyl native messenger!"
|
||||
echo "Run ':native' in Firefox to check."
|
||||
|
|
90
package.json
90
package.json
|
@ -2,64 +2,63 @@
|
|||
"name": "tridactyl",
|
||||
"version": "0.1.0",
|
||||
"description": "Vimperator/Pentadactyl successor",
|
||||
"browser": {
|
||||
"url": false,
|
||||
"fs": false,
|
||||
"https": false,
|
||||
"http": false,
|
||||
"path": false,
|
||||
"timers": false,
|
||||
"stream": "stream-browserify"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/css": "0.0.31",
|
||||
"@types/nearley": "^2.11.1",
|
||||
"command-line-args": "^5.1.1",
|
||||
"cleanslate": "^0.10.1",
|
||||
"csp-serdes": "github:cmcaine/csp-serdes",
|
||||
"css": "^3.0.0",
|
||||
"fuse.js": "^6.4.1",
|
||||
"jasmine-fail-fast": "^2.0.1",
|
||||
"mark.js": "^8.11.1",
|
||||
"editor-adapter": "^0.0.1",
|
||||
"esbuild": "^0.12.8",
|
||||
"fuse.js": "^6.4.6",
|
||||
"nearley": "^2.20.1",
|
||||
"ramda": "^0.27.1",
|
||||
"rss-parser": "^3.9.0",
|
||||
"semver-compare": "^1.0.0",
|
||||
"typedoc-default-themes": "^0.10.2"
|
||||
"stream-browserify": "^3.0.0",
|
||||
"tsdef": "^0.0.14",
|
||||
"typedoc": "^0.19.2",
|
||||
"typedoc-default-themes": "^0.12.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/firefox-webext-browser": "^78.0.1",
|
||||
"@types/jest": "^26.0.10",
|
||||
"@types/node": "^14.6.0",
|
||||
"@types/ramda": "^0.27.11",
|
||||
"@types/selenium-webdriver": "^4.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "^3.10.1",
|
||||
"@typescript-eslint/parser": "^3.10.1",
|
||||
"cleanslate": "^0.10.1",
|
||||
"copy-webpack-plugin": "^6.0.3",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-jsdoc": "^30.3.0",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.2",
|
||||
"eslint-plugin-sonarjs": "^0.5.0",
|
||||
"geckodriver": "^1.20.0",
|
||||
"@types/css": "0.0.33",
|
||||
"@types/firefox-webext-browser": "^82.0.0",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/nearley": "^2.11.2",
|
||||
"@types/selenium-webdriver": "^4.0.15",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "^4.29.0",
|
||||
"@typescript-eslint/parser": "^4.29.0",
|
||||
"command-line-args": "^5.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-jsdoc": "^36.0.6",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-sonarjs": "^0.7.0",
|
||||
"geckodriver": "^2.0.1",
|
||||
"jasmine-fail-fast": "^2.0.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest-webextension-mock": "^3.6.1",
|
||||
"marked": "^1.1.1",
|
||||
"nearley": "^2.19.6",
|
||||
"prettier": "^2.1.1",
|
||||
"selenium-webdriver": "^4.0.0-alpha.7",
|
||||
"shared-git-hooks": "^1.2.1",
|
||||
"source-map-loader": "^1.0.2",
|
||||
"jest-webextension-mock": "^3.7.17",
|
||||
"marked": "^2.1.3",
|
||||
"prettier": "^2.3.2",
|
||||
"selenium-webdriver": "^4.0.0-beta.3",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-loader": "^8.0.3",
|
||||
"ts-node": "^9.0.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.3.0",
|
||||
"tslint": "^5.20.1",
|
||||
"tslint-etc": "^1.13.6",
|
||||
"tslint-etc": "^1.13.10",
|
||||
"tslint-sonarts": "^1.9.0",
|
||||
"typedoc": "^0.18.0",
|
||||
"typescript": "^3.9.7",
|
||||
"uglify-es": "^3.3.9",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"web-ext": "^5.0.0",
|
||||
"web-ext-types": "^3.2.1",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"typescript": "^3.9.10",
|
||||
"web-ext": "^6.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "sh scripts/build.sh",
|
||||
"rebuild": "sh scripts/build.sh --quick",
|
||||
"clean": "rm -rf build generated",
|
||||
"forrest-run": "yarn run run",
|
||||
"jest": "jest --bail --runInBand",
|
||||
|
@ -69,7 +68,8 @@
|
|||
"run": "web-ext run -s build/ -u 'txti.es'",
|
||||
"test": "yarn run build && web-ext build --source-dir ./build --overwrite-dest && jest --silent",
|
||||
"update-buildsystem": "rm -rf src/node_modules; yarn run clean",
|
||||
"watch": "echo 'watch is broken, use build instead'; exit 0;"
|
||||
"watch": "echo 'watch is broken, use build instead'; exit 0;",
|
||||
"install": "git config core.hookspath hooks/"
|
||||
},
|
||||
"author": "Colin Caine",
|
||||
"repository": {
|
||||
|
|
11
privacy.md
Normal file
11
privacy.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
Tridactyl collects almost no data from its users. You may wish to note that:
|
||||
|
||||
- Tridactyl has [safeguards](https://github.com/tridactyl/tridactyl/blob/970a49bfb5eed00894d57fae4878c8adc7595ef8/src/state.ts#L80) to minimise the risk of inadvertently storing data locally from private browsing sessions.
|
||||
|
||||
- if you are using a "beta" build directly from GitHub or `tridactyl.cmcaine.co.uk`, Firefox will contact our `tridactyl.cmcaine.co.uk` server every few days to check for updates. We log the timestamps and IP addresses of all requests to this server to make debugging easier. Logs are retained for 90 days or until they reach 100MiB, whichever happens sooner.
|
||||
|
||||
- by default, Tridactyl makes a request to GitHub once a day so it can inform you of Tridactyl updates that you may have missed. You can disable this by setting `:set update.nag false`.
|
||||
|
||||
- if you have the native messenger installed, each time Tridactyl updates it will make a request to GitHub to check for updates to the native messenger. You can disable this by running `:set nativeinstallcmd echo`.
|
||||
|
||||
- if you donate via [GitHub sponsors](https://github.com/users/bovine3dom/sponsors), [Patreon](https://www.patreon.com/tridactyl) or [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY), your email address will be shared with us. We will never share your email addresses with third-parties.
|
147
readme.md
147
readme.md
|
@ -1,14 +1,39 @@
|
|||

|
||||
<h1 align="center">
|
||||
<br>
|
||||
<img src="src/static/logo/Tridactyl_200px.png" alt="Tridactyl Logo" width="200">
|
||||
<br>
|
||||
Tridactyl
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
# Tridactyl [](https://travis-ci.org/tridactyl/tridactyl) [![Matrix Chat][matrix-badge]][matrix-link] [![Gitter Chat][gitter-badge]][gitter-link]
|
||||
<h4>Replace Firefox's default control mechanism with one modelled on the one true editor, Vim. </h4>
|
||||
|
||||
Replace Firefox's default control mechanism with one modelled on the one true editor, Vim.
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/tridactyl/tridactyl"><img src="https://travis-ci.org/tridactyl/tridactyl.svg?branch=master" alt="Build Status"></a>
|
||||
<a href="https://matrix.to/#/#tridactyl:matrix.org"><img src="https://img.shields.io/badge/matrix-join%20chat-green" alt="Matrix Chat"></a>
|
||||
<a href="https://gitter.im/tridactyl/Lobby"><img src="https://badges.gitter.im/Join%20Chat.svg" alt="Join Gitter Chat"></a>
|
||||
<a href="https://discord.gg/DWbNGTAvmh"><img src="https://img.shields.io/discord/854326924402622474?color=%235865F2&label=discord" alt="Join Discord Chat"></a>
|
||||
<a href="https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/"><img src="https://img.shields.io/amo/rating/tridactyl-vim" alt="Mozilla Addon Store"></a>
|
||||
</p>
|
||||
|
||||

|
||||
<p align="center">
|
||||
<a href="#installation">Installation</a> •
|
||||
<a href="#changelog">Changelog</a> •
|
||||
<a href="#first-look">First Look</a> •
|
||||
<a href="#highlighted-features">Features</a> •
|
||||
<a href="#frequently-asked-questions-faq">FAQ</a> •
|
||||
<a href="#contributing">Contributing</a>
|
||||
</p>
|
||||
|
||||
## Installing
|
||||
<br>
|
||||
|
||||
[Simply click this link in Firefox to install our latest "beta" build][riskyclick]. If you want more options, read on.
|
||||
<p align="center">
|
||||
<img src="doc/AMO_screenshots/trishowcase.gif" alt="Tridactyl GIF">
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
|
||||
[Click this link in Firefox to install our latest "beta" build][riskyclick]. If it doesn't install automatically, you may need to 1) rename the extension from `.zip` to `.xpi` and 2) open it with Firefox; a fool-proof method is to go to `about:addons`, click the extensions tab, click the cog in the top right, then click "Install Add-on From File...". If you want more options, read on.
|
||||
|
||||
### Stable
|
||||
|
||||
|
@ -30,7 +55,7 @@ Tridactyl stable can be installed from the [Mozilla add-ons website (the AMO)][a
|
|||
|
||||
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.
|
||||
|
||||
## Migrating between beta and stable builds
|
||||
### Migrating between beta and stable builds
|
||||
|
||||
Our beta and stable versions store their configurations in separate places. To migrate between the two, see [the wiki](https://github.com/tridactyl/tridactyl/wiki/Migration-from-stable-to-beta).
|
||||
|
||||
|
@ -80,15 +105,18 @@ You can try `:help key` to know more about `key`. If it is an existing binding,
|
|||
- `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)
|
||||
- `g!` — Jumble words on page
|
||||
|
||||
#### Find mode
|
||||
|
||||
Find mode is still incomplete and uses the Firefox feature "Quick Find". This will be improved eventually.
|
||||
|
||||
- `/` — open the find search box
|
||||
- `/` — open the Quick Find search box
|
||||
- `/` then `<C-f>` — open the Find in page search box
|
||||
- `<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>`.
|
||||
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>` to search for things, use `<C-f>` after opening the Quick Find box (`/`), or any input field such as the address bar or search bar (use default browser shortcuts to activate these). To allow usage of `<C-f>` at any time, use `unbind <C-f>` to unset the scrollpage binding.
|
||||
|
||||
#### Bookmarks and quickmarks
|
||||
|
||||
|
@ -115,6 +143,7 @@ If you want to use Firefox's default `<C-b>` binding to open the bookmarks sideb
|
|||
- `u` — undo the last tab/window closure
|
||||
- `gt`/`gT` — go to the next/previous tab
|
||||
- `g^ OR g0`/`g$` — go to the first/last tab
|
||||
- `ga` — go to the tab currently playing audio
|
||||
- `<C-^>` — go to the last active tab
|
||||
- `b` — bring up a list of open tabs in the current window; you can type the tab ID or part of the title or URL to choose a tab
|
||||
|
||||
|
@ -174,7 +203,18 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
- How can I change the colors or theme used by Tridactyl?
|
||||
|
||||
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.
|
||||
To use one of the built in themes use: `:colors <color>`. The current options are:
|
||||
|
||||
- default
|
||||
- dark (authored by @furgerf)
|
||||
- shydactyl (authored by @atrnh)
|
||||
- greenmat (authored by @caputchinefrobles)
|
||||
- halloween
|
||||
- quake
|
||||
- quakelight
|
||||
- midnight (authored by @karizma)
|
||||
|
||||
Tridactyl can also load themes from disk or URL, 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.
|
||||
|
||||
- How to remap keybindings? or How can I bind keys using the control/alt key modifiers (eg: `ctrl+^`)?
|
||||
|
||||
|
@ -184,7 +224,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
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.
|
||||
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.
|
||||
|
||||
You can also create site specific binds with `bindurl [url] ...`
|
||||
|
||||
|
@ -226,7 +266,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
- Does anyone actually use Tridactyl?
|
||||
|
||||
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 4600 of them.
|
||||
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), but, as of a while ago, you can't see that link if you aren't listed as a Tridactyl developer on the AMO. 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 4600 of them.
|
||||
|
||||
- How do I prevent websites from stealing focus?
|
||||
|
||||
|
@ -236,11 +276,11 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
### 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.
|
||||
We gratefully accept donations via [GitHub Sponsors](https://github.com/users/bovine3dom/sponsorship) (we receive 100% of your donation), [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY) (about 70% of your donation makes it to our bank account after fees) and [Patreon](https://www.patreon.com/tridactyl) (about 70% of your donation makes it to our account). If you can, please make this a monthly donation as it makes it much easier to plan. People who donate more than 10USD a month via GitHub or Patreon get a special monthly "tips and tricks" newsletter - see an example [here](https://github.com/tridactyl/tridactyl/blob/master/doc/newsletters/tips-and-tricks/1-hint-css-selectors.md). All GitHub and Patreon donors get a quarterly newsletter on Tridactyl development.
|
||||
|
||||
<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.
|
||||
Funds currently ensure that bovine3dom can afford to work on Tridactyl about one day a week.
|
||||
|
||||
### Merchandise
|
||||
|
||||
|
@ -259,17 +299,17 @@ yarn install
|
|||
yarn run build
|
||||
```
|
||||
|
||||
Each time package.json or package-lock.json change after you checkout or pull, you should run `yarn install` again.
|
||||
Each time package.json or yarn.lock change after you checkout or pull, our git hook will try to run `yarn install` again. If it doesn't you should do it manually.
|
||||
|
||||
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.
|
||||
Addon is built in tridactyl/build. Load it as a temporary addon in firefox with `about:debugging` or see [Development loop](#Development-loop).
|
||||
|
||||
If you want to install a local copy of the add-on into your developer or nightly build of firefox then you can enable installing unsigned add-ons and then build it like so:
|
||||
If you want to install a local copy of the add-on into your developer or nightly build of Firefox then you can enable installing unsigned add-ons and then build it like so:
|
||||
|
||||
```
|
||||
# Build tridactyl if you haven't done that yet
|
||||
yarn run build
|
||||
# Package for a browser
|
||||
"$(yarn bin)/web-ext" build -s build
|
||||
scripts/sign nosignbeta
|
||||
```
|
||||
|
||||
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.
|
||||
|
@ -294,65 +334,6 @@ If you are on a distribution which builds Firefox with `--with-unsigned-addon-sc
|
|||
[pyinstaller]: https://www.pyinstaller.org
|
||||
[gpg4win]: https://www.gpg4win.org
|
||||
|
||||
<!--- ## Disable GPG signing for now, until decided otherwise later
|
||||
|
||||
### Cryptographically Verifying the Compiled Native Binary on Windows
|
||||
|
||||
- `native_main.py` is compiled to `native_main.exe` for Windows using [PyInstaller][pyinstaller]. The goal is to relieve Tridactyl users on Windows from having to install the whole Python 3 distribution.
|
||||
|
||||
- Due to `native_main.exe` being a binary-blob and difficult to easily review like the plain-text `native_main.py` counterpart, it is **strongly** recommended the users verify the SHA-256 hash and GPG signatures using the following commands on Powershell.
|
||||
|
||||
**Verifying SHA-256 Hash**
|
||||
|
||||
```
|
||||
## Change directory to Tridactyl's native-messanger directory
|
||||
PS C:\> cd "$env:HOME\.tridactyl"
|
||||
|
||||
## Run `dir` and check `native_main.exe` is found
|
||||
PS C:\Users\{USERNAME}\.tridactyl> dir
|
||||
|
||||
## Download `native_main.exe.sha256` containing the SHA-256 sum
|
||||
PS C:\Users\{USERNAME}\.tridactyl> iwr https://raw.githubusercontent.com/gsbabil/tridactyl/master/native/native_main.exe.sha256 -OutFile native_main.exe.sha256
|
||||
|
||||
## Print the SHA-256 sum from `native_main.exe.sha256`
|
||||
PS C:\Users\{USERNAME}\.tridactyl> (Get-FileHash native_main.exe -Algorithm SHA256).Hash.ToLower()
|
||||
|
||||
## Compute SHA-256 sum from `native_main.exe`
|
||||
PS C:\Users\{USERNAME}\.tridactyl> Get-Content -Path native_main.exe.sha256 | %{$_ .Split(' ')[0]}
|
||||
|
||||
## Compare results of the of the last two commands ...
|
||||
```
|
||||
|
||||
**Verifying OpenPGP Signature***
|
||||
|
||||
- First, download [`GPG4Win`][gpg4win] from this website and
|
||||
install in on your system
|
||||
|
||||
- Once `gpg2` is on your path, go to Powershell and run the
|
||||
following commands to verify OpenPGP signature:
|
||||
|
||||
```
|
||||
## Change directory to Tridactyl's native-messanger directory
|
||||
PS C:\> cd "$env:HOME\.tridactyl"
|
||||
|
||||
## Run `dir` and check `native_main.exe` is found
|
||||
PS C:\Users\{USERNAME}\.tridactyl> dir
|
||||
|
||||
## Download `native_main.exe.sig` containing the OpenPGP signature
|
||||
PS C:\Users\{USERNAME}\.tridactyl> iwr https://raw.githubusercontent.com/gsbabil/tridactyl/master/native/native_main.exe.sig -OutFile native_main.exe.sig
|
||||
|
||||
## Download `gsbabil-pub.asc` to verify the signature
|
||||
PS C:\Users\{USERNAME}\.tridactyl> iwr https://raw.githubusercontent.com/gsbabil/tridactyl/master/native/gsbabil-pub.asc -OutFile gsbabil-pub.asc
|
||||
|
||||
## Import `gsbabil-pub.asc` into your GPG key-ring
|
||||
PS C:\Users\{USERNAME}\.tridactyl> gpg2 --armor --import gsbabil-pub.asc
|
||||
|
||||
## Verify signature using `gpg2`
|
||||
PS C:\Users\{USERNAME}\.tridactyl> gpg2 --verify .\native_main.exe.sig .\native_main.exe
|
||||
```
|
||||
|
||||
--->
|
||||
|
||||
### Development loop
|
||||
|
||||
```
|
||||
|
@ -363,13 +344,15 @@ yarn run build & yarn run run
|
|||
|
||||
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.
|
||||
|
||||
You can speed up the build process after your first build by using `yarn run rebuild` instead. This skips rebuilding the metadata (used in completions), documentation, new tab page, and tutor, so don't use it if that's what you're trying to test.
|
||||
|
||||
### Committing
|
||||
|
||||
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!
|
||||
Ask in `#tridactyl` on [matrix.org][matrix-link], Libera, [gitter][gitter-link], or [Discord](https://discord.gg/DWbNGTAvmh). We're friendly!
|
||||
|
||||
Default keybindings are currently best discovered by reading the [default config](./src/lib/config.ts).
|
||||
|
||||
|
@ -396,12 +379,12 @@ Other objectives:
|
|||
|
||||
The logo was designed by Jake Beazley using free vector art by <a target="_blank" href="https://www.Vecteezy.com">www.Vecteezy.com</a>
|
||||
|
||||
[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
|
||||
[gitter-badge]: /static/badges/gitter-badge.svg
|
||||
[gitter-link]: https://gitter.im/tridactyl/Lobby
|
||||
[matrix-badge]: https://matrix.to/img/matrix-badge.svg
|
||||
[matrix-link]: https://riot.im/app/#/room/#tridactyl:matrix.org
|
||||
[matrix-badge]: /static/badges/matrix-badge.svg
|
||||
[matrix-link]: https://app.element.io/#/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
|
||||
[amo]: https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim?utm_source=github.com&utm_content=readme.md
|
||||
[migratelink]: https://github.com/tridactyl/tridactyl/issues/79#issuecomment-351132451
|
||||
|
|
|
@ -4,10 +4,18 @@ set -e
|
|||
err() { echo "error: line $(caller)"; }
|
||||
trap err ERR
|
||||
|
||||
mkdir -p .build_cache
|
||||
cd src/static
|
||||
|
||||
authors="../../build/static/authors.html"
|
||||
|
||||
sed "/REPLACETHIS/,$ d" authors.html > "$authors"
|
||||
git shortlog -sn HEAD | cut -c8- | awk '!seen[$0]++' | sed 's/^/<p>/' | sed 's/$/<\/p>/' >> "$authors"
|
||||
|
||||
# If we're in a git repo, refresh the cache
|
||||
if [ -d "../../.git/" ]; then
|
||||
git shortlog -sn HEAD | cut -c8- | awk '!seen[$0]++' | sed 's/^/<p>/' | sed 's/$/<\/p>/' > ../../.build_cache/authors
|
||||
fi
|
||||
|
||||
cat ../../.build_cache/authors >> "$authors"
|
||||
|
||||
sed "1,/REPLACETHIS/ d" authors.html >> "$authors"
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
imports=$(find src/static/themes -name '*.css'| awk -F"/" '{ printf "@import url('\''../%s/%s/%s'\'');\n", $3, $4, $5 }')
|
||||
|
||||
|
||||
for css in build/static/css/*.css; do
|
||||
printf '%s\n%s\n' "$imports" "$(cat "$css")" > "$css"
|
||||
done
|
|
@ -2,8 +2,21 @@
|
|||
|
||||
set -e
|
||||
|
||||
for arg in "$@"
|
||||
do
|
||||
case $arg in
|
||||
--quick)
|
||||
QUICK_BUILD=1
|
||||
shift
|
||||
;;
|
||||
--old-native)
|
||||
OLD_NATIVE=1
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
CLEANSLATE="node_modules/cleanslate/docs/files/cleanslate.css"
|
||||
TRIDACTYL_LOGO="src/static/logo/Tridactyl_64px.png"
|
||||
|
||||
isWindowsMinGW() {
|
||||
is_mingw="False"
|
||||
|
@ -36,23 +49,54 @@ 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
|
||||
# You can use `--quick` to test out small changes without updating docs / metadata etc.
|
||||
# If you get weird behaviour just run a full build
|
||||
if [ "$QUICK_BUILD" != "1" ]; then
|
||||
|
||||
# It's important to generate the metadata before the documentation because
|
||||
# missing imports might break documentation generation on clean builds
|
||||
"$(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/lib/config.ts
|
||||
# .bracketexpr.generated.ts is needed for metadata generation
|
||||
"$(yarn bin)/nearleyc" src/grammars/bracketexpr.ne > \
|
||||
src/grammars/.bracketexpr.generated.ts
|
||||
|
||||
scripts/newtab.md.sh
|
||||
scripts/make_tutorial.sh
|
||||
scripts/make_docs.sh
|
||||
# It's important to generate the metadata before the documentation because
|
||||
# missing imports might break documentation generation on clean builds
|
||||
"$(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/lib/config.ts
|
||||
|
||||
if [ "$1" != "--no-native" ]; then
|
||||
scripts/newtab.md.sh
|
||||
scripts/make_tutorial.sh
|
||||
scripts/make_docs.sh
|
||||
|
||||
tsc --project tsconfig.json --noEmit
|
||||
else
|
||||
|
||||
echo "Warning: dirty rebuild. Skipping docs, metadata and type checking..."
|
||||
|
||||
fi
|
||||
|
||||
# Actually build the thing
|
||||
|
||||
mkdir -p buildtemp
|
||||
node scripts/esbuild.js
|
||||
mv buildtemp/* build/
|
||||
rmdir buildtemp
|
||||
|
||||
# Copy extra static files across
|
||||
|
||||
cp src/manifest.json build/
|
||||
cp -r src/static build
|
||||
cp -r generated/static build
|
||||
cp issue_template.md build/
|
||||
|
||||
# Remove large unused files
|
||||
|
||||
rm build/static/logo/Tridactyl.psd
|
||||
rm build/static/logo/Tridactyl_1024px.png
|
||||
|
||||
# "temporary" fix until we can install new native on CI: install the old native messenger
|
||||
if [ "$OLD_NATIVE" = "1" ]; then
|
||||
if [ "$(isWindowsMinGW)" = "True" ]; then
|
||||
powershell \
|
||||
-NoProfile \
|
||||
|
@ -64,9 +108,6 @@ if [ "$1" != "--no-native" ]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
webpack --display errors-only --bail
|
||||
|
||||
scripts/bodgecss.sh
|
||||
scripts/authors.sh
|
||||
|
||||
if [ -e "$CLEANSLATE" ] ; then
|
||||
|
@ -74,14 +115,3 @@ if [ -e "$CLEANSLATE" ] ; then
|
|||
else
|
||||
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
|
||||
|
|
|
@ -34,8 +34,13 @@ eslintUgly() {
|
|||
local IFS=$'\n'
|
||||
local tmpdir
|
||||
|
||||
mkdir -p .tmp
|
||||
tmpdir=$(mktemp --tmpdir=".tmp/" -d "tslint.XXXXXXXXX")
|
||||
mkdir -p ".tmp"
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
tmpdir=$(gmktemp --tmpdir=".tmp/" -d "tslint.XXXXXXXXX")
|
||||
else
|
||||
tmpdir=$(mktemp --tmpdir=".tmp/" -d "tslint.XXXXXXXXX")
|
||||
fi
|
||||
|
||||
for jsfile in "$@"; do
|
||||
tmpfile="$tmpdir/$jsfile"
|
||||
mkdir -p "$(dirname "$tmpfile")"
|
||||
|
|
11
scripts/esbuild.js
Normal file
11
scripts/esbuild.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const esbuild = require('esbuild')
|
||||
|
||||
for (let f of ["content", "background", "help", "newtab", "commandline_frame"]) {
|
||||
esbuild.build({
|
||||
entryPoints: [`src/${f}.ts`],
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
target: "firefox68",
|
||||
outfile: `buildtemp/${f}.js`,
|
||||
}).catch(() => process.exit(1))
|
||||
}
|
16
scripts/sign
16
scripts/sign
|
@ -4,7 +4,7 @@ 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 && (yarn run web-ext sign -s build --api-key "$AMOKEY" --api-secret "$AMOSECRET" "$@" || true))
|
||||
}
|
||||
|
||||
publish_beta_nonewtab() {
|
||||
|
@ -23,6 +23,7 @@ publish_beta() {
|
|||
scripts/version.js beta
|
||||
sed 's/"name": "Tridactyl"/"name": "Tridactyl: Beta"/' -i build/manifest.json
|
||||
sign_and_submit
|
||||
tar --exclude-from=<(grep -v .build_cache/ .gitignore) --exclude-vcs -czf ../../public_html/betas/tridactyl_source_beta.tar.gz .
|
||||
}
|
||||
|
||||
build_no_sign_beta(){
|
||||
|
@ -31,9 +32,9 @@ build_no_sign_beta(){
|
|||
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
|
||||
yarn run web-ext build --source-dir ./build --overwrite-dest
|
||||
for f in web-ext-artifacts/*.zip; do
|
||||
mv $f ${f%.zip}.xpi
|
||||
mv "$f" "${f%.zip}".xpi
|
||||
done
|
||||
}
|
||||
|
||||
|
@ -42,9 +43,9 @@ build_no_sign_stable(){
|
|||
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
|
||||
yarn run web-ext build --source-dir ./build --overwrite-dest
|
||||
for f in web-ext-artifacts/*.zip; do
|
||||
mv $f ${f%.zip}.xpi
|
||||
mv "$f" "${f%.zip}".xpi
|
||||
done
|
||||
}
|
||||
|
||||
|
@ -53,7 +54,7 @@ publish_stable() {
|
|||
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=<(grep -v .build_cache/ .gitignore) --exclude-vcs -czf ../../public_html/betas/tridactyl_source.tar.gz .
|
||||
}
|
||||
|
||||
case $1 in
|
||||
|
@ -61,5 +62,6 @@ case $1 in
|
|||
nosignstable) build_no_sign_stable;;
|
||||
nosignbeta) build_no_sign_beta;;
|
||||
nonewtab) publish_beta_nonewtab;;
|
||||
*|beta) publish_beta;;
|
||||
beta) publish_beta;;
|
||||
*) publish_beta;;
|
||||
esac
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { exec } = require("child_process")
|
||||
const fs = require("fs")
|
||||
|
||||
function bump_version(versionstr, component = 2) {
|
||||
const versionarr = versionstr.split(".")
|
||||
|
@ -12,21 +13,37 @@ function bump_version(versionstr, component = 2) {
|
|||
}
|
||||
|
||||
async function add_beta(versionstr) {
|
||||
return new Promise((resolve, err) => {
|
||||
exec("git rev-list --count HEAD", (execerr, stdout, stderr) => {
|
||||
if (execerr) err(execerr)
|
||||
resolve(versionstr + "pre" + stdout.trim())
|
||||
await fs.promises.mkdir(".build_cache", {recursive: true})
|
||||
try {
|
||||
await fs.promises.access(".git")
|
||||
await new Promise((resolve, err) => {
|
||||
exec("git rev-list --count HEAD > .build_cache/count", (execerr, stdout, stderr) => {
|
||||
if (execerr) err(execerr)
|
||||
resolve(stdout.trim())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
catch {
|
||||
; // Not in a git directory - don't do anything
|
||||
}
|
||||
return versionstr + "pre" + (await fs.promises.readFile(".build_cache/count", {encoding: "utf8"})).trim()
|
||||
}
|
||||
|
||||
async function get_hash() {
|
||||
return new Promise((resolve, err) => {
|
||||
exec("git rev-parse --short HEAD", (execerr, stdout, stderr) => {
|
||||
if (execerr) err(execerr)
|
||||
resolve(stdout.trim())
|
||||
await fs.promises.mkdir(".build_cache", {recursive: true})
|
||||
try {
|
||||
await fs.promises.access(".git")
|
||||
await new Promise((resolve, err) => {
|
||||
exec("git rev-parse --short HEAD > .build_cache/hash", (execerr, stdout, stderr) => {
|
||||
if (execerr) err(execerr)
|
||||
resolve(stdout.trim())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
catch {
|
||||
; // Not in a git directory - don't do anything
|
||||
}
|
||||
return (await fs.promises.readFile(".build_cache/hash", {encoding: "utf8"})).trim()
|
||||
}
|
||||
|
||||
function make_update_json(versionstr) {
|
||||
|
@ -102,7 +119,7 @@ async function main() {
|
|||
make_update_json(manifest.version),
|
||||
)
|
||||
} catch(e) {
|
||||
console.warn("updates.json wasn't updated: " + e)
|
||||
console.warn("Unless you're the buildbot, ignore this error: " + e)
|
||||
}
|
||||
|
||||
// Save manifest.json
|
||||
|
|
|
@ -27,6 +27,8 @@ import * as omnibox from "@src/background/omnibox"
|
|||
import * as R from "ramda"
|
||||
import * as webrequests from "@src/background/webrequests"
|
||||
import * as commands from "@src/background/commands"
|
||||
import * as meta from "@src/background/meta"
|
||||
import * as Logging from "@src/lib/logging"
|
||||
|
||||
// Add various useful modules to the window for debugging
|
||||
;(window as any).tri = Object.assign(Object.create(null), {
|
||||
|
@ -44,10 +46,11 @@ import * as commands from "@src/background/commands"
|
|||
state,
|
||||
webext,
|
||||
webrequests,
|
||||
l: prom => prom.then(console.log).catch(console.error),
|
||||
l: (prom: Promise<any>) => prom.then(console.log).catch(console.error),
|
||||
contentLocation: window.location,
|
||||
R,
|
||||
perf,
|
||||
meta,
|
||||
})
|
||||
|
||||
import { HintingCmds } from "@src/background/hinting"
|
||||
|
@ -84,23 +87,23 @@ browser.tabs.onActivated.addListener(ev => {
|
|||
/**
|
||||
* Declare Tab Event Listeners
|
||||
*/
|
||||
browser.tabs.onRemoved.addListener((tabId) => {
|
||||
browser.tabs.onRemoved.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_close", [tabId])
|
||||
})
|
||||
// Fired when a tab is attached to a window, for example because it was moved between windows.
|
||||
browser.tabs.onAttached.addListener((tabId) => {
|
||||
browser.tabs.onAttached.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_attached", [tabId])
|
||||
})
|
||||
// Fired when a tab is created. Note that the tab's URL may not be set at the time this event fired.
|
||||
browser.tabs.onCreated.addListener((tabId) => {
|
||||
browser.tabs.onCreated.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_created", [tabId])
|
||||
})
|
||||
// Fired when a tab is detached from a window, for example because it is being moved between windows.
|
||||
browser.tabs.onDetached.addListener((tabId) => {
|
||||
browser.tabs.onDetached.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_detached", [tabId])
|
||||
})
|
||||
// Fired when a tab is moved within a window.
|
||||
browser.tabs.onMoved.addListener((tabId) => {
|
||||
browser.tabs.onMoved.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_moved", [tabId])
|
||||
})
|
||||
|
||||
|
@ -114,16 +117,23 @@ browser.webNavigation.onDOMContentLoaded.addListener(() => {
|
|||
// Prevent Tridactyl from being updated while it is running in the hope of fixing #290
|
||||
browser.runtime.onUpdateAvailable.addListener(_ => undefined)
|
||||
|
||||
const autocmd_logger = new Logging.Logger("autocmds")
|
||||
browser.runtime.onStartup.addListener(() => {
|
||||
config.getAsync("autocmds", "TriStart").then(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] === ".*") {
|
||||
autocmd_logger.debug(
|
||||
`TriStart matched ${hosts[0]}: ${aucmds[hosts[0]]}`,
|
||||
)
|
||||
controller.acceptExCmd(aucmds[hosts[0]])
|
||||
} else {
|
||||
native.run("hostname").then(hostname => {
|
||||
for (const host of hosts) {
|
||||
if (new RegExp(host).exec(hostname.content)) {
|
||||
autocmd_logger.debug(
|
||||
`TriStart matched ${host}: ${aucmds[host]}`,
|
||||
)
|
||||
controller.acceptExCmd(aucmds[host])
|
||||
}
|
||||
}
|
||||
|
@ -269,7 +279,6 @@ omnibox.init()
|
|||
|
||||
// }}}
|
||||
|
||||
|
||||
setTimeout(config.update, 5000)
|
||||
|
||||
commands.updateListener()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as useractions from "@src/background/user_actions"
|
||||
import { useractions } from "@src/background/user_actions"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as keyseq from "@src/lib/keyseq"
|
||||
import * as controller from "@src/lib/controller"
|
||||
|
||||
function makelistener(commands) {
|
||||
function makelistener(commands: Array<browser.commands.Command>) {
|
||||
return (command_name: string) => {
|
||||
const command = commands.filter(c => c.name == command_name)[0]
|
||||
const exstring = config.get(
|
||||
|
|
|
@ -13,7 +13,10 @@ export async function source(filename = "auto") {
|
|||
return true
|
||||
}
|
||||
|
||||
async function fetchConfig(url: string) {
|
||||
/*
|
||||
* This should be moved out to a library but I am lazy
|
||||
*/
|
||||
export async function fetchText(url: string) {
|
||||
const response = await fetch(url)
|
||||
const reader = response.body.getReader()
|
||||
let rctext = ""
|
||||
|
@ -24,6 +27,7 @@ async function fetchConfig(url: string) {
|
|||
rctext += decoder.decode(chunk)
|
||||
}
|
||||
}
|
||||
const fetchConfig = fetchText
|
||||
|
||||
export async function sourceFromUrl(url: string) {
|
||||
const rctext = await fetchConfig(url)
|
||||
|
@ -62,5 +66,10 @@ export function rcFileToExCmds(rcText: string): string[] {
|
|||
const excmds = joined.split("\n")
|
||||
|
||||
// Remove empty and comment lines
|
||||
return excmds.filter(x => /\S/.test(x) && !x.trim().startsWith('"'))
|
||||
return excmds.filter(
|
||||
x =>
|
||||
/\S/.test(x) &&
|
||||
!x.trim().startsWith('"') &&
|
||||
!x.trim().startsWith("#"),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import * as Native from "@src/lib/native"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as R from "ramda"
|
||||
import { getDownloadFilenameForUrl } from "@src/lib/url_util"
|
||||
|
||||
/** Construct an object URL string from a given data URL
|
||||
|
@ -78,11 +79,17 @@ export async function downloadUrl(url: string, saveAs: boolean) {
|
|||
*
|
||||
* Note: this requires a native messenger >=0.1.9. Make sure to nativegate for this.
|
||||
*
|
||||
* @param url the URL to download
|
||||
* @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.
|
||||
* @param If true, overwrite the destination file, returns error code 1 otherwise if file exists
|
||||
* @param If true, cleans up temporary downloaded source file e.g. in $HOME/Downlods/downloaded.doc when the move operation fails e.g. due to target destination exists, OS error etc.
|
||||
*/
|
||||
export async function downloadUrlAs(url: string, saveAs: string) {
|
||||
export async function downloadUrlAs(
|
||||
url: string,
|
||||
saveAs: string,
|
||||
overwrite: boolean,
|
||||
cleanup: boolean,
|
||||
) {
|
||||
if (!(await Native.nativegate("0.1.9", true))) return
|
||||
const urlToSave = new URL(url)
|
||||
|
||||
|
@ -125,15 +132,28 @@ export async function downloadUrlAs(url: string, saveAs: string) {
|
|||
const operation = await Native.move(
|
||||
downloadItem.filename,
|
||||
saveAs,
|
||||
overwrite,
|
||||
cleanup,
|
||||
)
|
||||
if (operation.code !== 0) {
|
||||
const code2human = n =>
|
||||
R.defaultTo(
|
||||
"Unknown error",
|
||||
{ 1: "File already exists", 2: "Other OS error" }[
|
||||
n
|
||||
],
|
||||
)
|
||||
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.`,
|
||||
`${code2human(operation.code)}. '${
|
||||
downloadItem.filename
|
||||
}' could not be moved to '${saveAs}'. Error code: ${
|
||||
operation.code
|
||||
}`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
resolve(operation)
|
||||
resolve(downloadItem.filename)
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { messageActiveTab } from "@src/lib/messaging.ts"
|
||||
import * as _EditorCmds from "@src/lib/editor.ts"
|
||||
import { messageActiveTab } from "@src/lib/messaging"
|
||||
import * as _EditorCmds from "@src/lib/editor"
|
||||
|
||||
type cmdsType = typeof _EditorCmds
|
||||
type ArgumentsType<T> = T extends (elem, ...args: infer U) => any ? U : never
|
||||
|
|
23
src/background/meta.ts
Normal file
23
src/background/meta.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { messageTab } from "@src/lib/messaging"
|
||||
|
||||
export async function getTridactylTabs(
|
||||
tabs?: browser.tabs.Tab[],
|
||||
negate = false,
|
||||
) {
|
||||
tabs = tabs || (await browser.tabs.query({ currentWindow: true }))
|
||||
const tridactyl_tabs: browser.tabs.Tab[] = []
|
||||
await Promise.all(
|
||||
tabs.map(async tab => {
|
||||
try {
|
||||
// This doesn't actually return "true" like it is supposed to
|
||||
await messageTab(tab.id, "alive")
|
||||
!negate && tridactyl_tabs.push(tab)
|
||||
return true
|
||||
} catch (e) {
|
||||
negate && tridactyl_tabs.push(tab)
|
||||
return false
|
||||
}
|
||||
}),
|
||||
)
|
||||
return tridactyl_tabs
|
||||
}
|
|
@ -6,7 +6,6 @@ import * as controller from "@src/lib/controller"
|
|||
|
||||
export function inputEnteredListener(
|
||||
input: string,
|
||||
disposition: browser.omnibox.OnInputEnteredDisposition,
|
||||
) {
|
||||
controller.acceptExCmd(input)
|
||||
}
|
||||
|
|
|
@ -6,27 +6,18 @@
|
|||
|
||||
import * as excmds from "@src/.excmds_background.generated"
|
||||
import * as R from "ramda"
|
||||
import { messageTab } from "@src/lib/messaging"
|
||||
import * as config from "@src/lib/config"
|
||||
import { getTridactylTabs } from "@src/background/meta"
|
||||
|
||||
export function escapehatch() {
|
||||
// Only works if called via commands API command - fail silently if called otherwise
|
||||
browser.sidebarAction.open().catch()
|
||||
browser.sidebarAction.close().catch()
|
||||
function escapehatch() {
|
||||
if (config.get("escapehatchsidebarhack") == "true") {
|
||||
// Only works if called via commands API command - fail silently if called otherwise
|
||||
browser.sidebarAction.open().catch()
|
||||
browser.sidebarAction.close().catch()
|
||||
}
|
||||
;(async () => {
|
||||
const tabs = await browser.tabs.query({ currentWindow: true })
|
||||
const tridactyl_tabs: browser.tabs.Tab[] = []
|
||||
await Promise.all(
|
||||
tabs.map(async tab => {
|
||||
try {
|
||||
// This doesn't actually return "true" like it is supposed to
|
||||
await messageTab(tab.id, "alive")
|
||||
tridactyl_tabs.push(tab)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
)
|
||||
const tridactyl_tabs = await getTridactylTabs(tabs)
|
||||
const curr_pos = tabs.filter(t => t.active)[0].index
|
||||
|
||||
// If Tridactyl isn't running in any tabs in the current window open a new tab
|
||||
|
@ -44,3 +35,7 @@ export function escapehatch() {
|
|||
return browser.tabs.update(best.id, { active: true })
|
||||
})()
|
||||
}
|
||||
|
||||
export const useractions: Record<string, () => void> = {
|
||||
escapehatch,
|
||||
}
|
||||
|
|
|
@ -17,49 +17,54 @@
|
|||
|
||||
/** Script used in the commandline iframe. Communicates with background. */
|
||||
|
||||
import * as perf from "@src/perf"
|
||||
import "@src/lib/number.clamp"
|
||||
import "@src/lib/html-tagged-template"
|
||||
import { TabAllCompletionSource } from "@src/completions/TabAll"
|
||||
import * as SELF from "@src/commandline_frame"
|
||||
import { CompletionSourceFuse } from "@src/completions"
|
||||
import { AproposCompletionSource } from "@src/completions/Apropos"
|
||||
import { BindingsCompletionSource } from "@src/completions/Bindings"
|
||||
import { BufferCompletionSource } from "@src/completions/Tab"
|
||||
import { BmarkCompletionSource } from "@src/completions/Bmark"
|
||||
import { ExcmdCompletionSource } from "@src/completions/Excmd"
|
||||
import { CompositeCompletionSource } from "@src/completions/Composite"
|
||||
import { ExcmdCompletionSource } from "@src/completions/Excmd"
|
||||
import { ExtensionsCompletionSource } from "@src/completions/Extensions"
|
||||
import { FileSystemCompletionSource } from "@src/completions/FileSystem"
|
||||
import { GuisetCompletionSource } from "@src/completions/Guiset"
|
||||
import { HelpCompletionSource } from "@src/completions/Help"
|
||||
import { AproposCompletionSource } from "@src/completions/Apropos"
|
||||
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 { BufferCompletionSource } from "@src/completions/Tab"
|
||||
import { TabAllCompletionSource } from "@src/completions/TabAll"
|
||||
import { ThemeCompletionSource } from "@src/completions/Theme"
|
||||
import { WindowCompletionSource } from "@src/completions/Window"
|
||||
import { ExtensionsCompletionSource } from "@src/completions/Extensions"
|
||||
import { contentState } from "@src/content/state_content"
|
||||
import { theme } from "@src/content/styling"
|
||||
import { getCommandlineFns } from "@src/lib/commandline_cmds"
|
||||
import * as tri_editor from "@src/lib/editor"
|
||||
import "@src/lib/DANGEROUS-html-tagged-template"
|
||||
import Logger from "@src/lib/logging"
|
||||
import * as Messaging from "@src/lib/messaging"
|
||||
import "@src/lib/number.clamp"
|
||||
import state from "@src/state"
|
||||
import * as State from "@src/state"
|
||||
import Logger from "@src/lib/logging"
|
||||
import { theme } from "@src/content/styling"
|
||||
|
||||
import * as genericParser from "@src/parsers/genericmode"
|
||||
import * as tri_editor from "@src/lib/editor"
|
||||
import * as perf from "@src/perf"
|
||||
import state, * as State from "@src/state"
|
||||
import * as R from "ramda"
|
||||
import { KeyEventLike } from "@src/lib/keyseq"
|
||||
import { TabGroupCompletionSource } from "@src/completions/TabGroup"
|
||||
|
||||
/** @hidden **/
|
||||
const logger = new Logger("cmdline")
|
||||
|
||||
/** @hidden **/
|
||||
const commandline_state = {
|
||||
activeCompletions: undefined,
|
||||
activeCompletions: undefined as CompletionSourceFuse[],
|
||||
clInput: window.document.getElementById(
|
||||
"tridactyl-input",
|
||||
) as HTMLInputElement,
|
||||
clear,
|
||||
cmdline_history_position: 0,
|
||||
completionsDiv: window.document.getElementById("completions"),
|
||||
fns: undefined,
|
||||
fns: undefined as ReturnType<typeof getCommandlineFns>,
|
||||
getCompletion,
|
||||
history,
|
||||
/** @hidden
|
||||
|
@ -112,6 +117,7 @@ export function enableCompletions() {
|
|||
TabAllCompletionSource,
|
||||
BufferCompletionSource,
|
||||
ExcmdCompletionSource,
|
||||
ThemeCompletionSource,
|
||||
CompositeCompletionSource,
|
||||
FileSystemCompletionSource,
|
||||
GuisetCompletionSource,
|
||||
|
@ -144,7 +150,7 @@ export function enableCompletions() {
|
|||
/* document.addEventListener("DOMContentLoaded", enableCompletions) */
|
||||
|
||||
/** @hidden **/
|
||||
const noblur = e => setTimeout(() => commandline_state.clInput.focus(), 0)
|
||||
const noblur = () => setTimeout(() => commandline_state.clInput.focus(), 0)
|
||||
|
||||
/** @hidden **/
|
||||
export function focus() {
|
||||
|
@ -164,6 +170,11 @@ const keyParser = keys => genericParser.parser("exmaps", keys)
|
|||
let history_called = false
|
||||
/** @hidden **/
|
||||
let prev_cmd_called_history = false
|
||||
|
||||
// Save programmer time by generating an immediately resolved promise
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const QUEUE: Promise<any>[] = [(async () => {})()]
|
||||
|
||||
/** @hidden **/
|
||||
commandline_state.clInput.addEventListener(
|
||||
"keydown",
|
||||
|
@ -192,13 +203,19 @@ commandline_state.clInput.addEventListener(
|
|||
if (response.value.startsWith("ex.")) {
|
||||
const [funcname, ...args] = response.value.slice(3).split(/\s+/)
|
||||
|
||||
if (args.length === 0) {
|
||||
commandline_state.fns[funcname]()
|
||||
} else {
|
||||
commandline_state.fns[funcname](args.join(" "))
|
||||
}
|
||||
|
||||
prev_cmd_called_history = history_called
|
||||
QUEUE[QUEUE.length - 1].then(() => {
|
||||
QUEUE.push(
|
||||
// Abuse async to wrap non-promises in a promise
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
(async () =>
|
||||
commandline_state.fns[
|
||||
funcname as keyof typeof commandline_state.fns
|
||||
](
|
||||
args.length === 0 ? undefined : args.join(" "),
|
||||
))(),
|
||||
)
|
||||
prev_cmd_called_history = history_called
|
||||
})
|
||||
} else {
|
||||
// Send excmds directly to our own tab, which fixes the
|
||||
// old bug where a command would be issued in one tab but
|
||||
|
@ -240,14 +257,22 @@ let onInputPromise: Promise<any> = Promise.resolve()
|
|||
/** @hidden **/
|
||||
commandline_state.clInput.addEventListener("input", () => {
|
||||
const exstr = commandline_state.clInput.value
|
||||
contentState.current_cmdline = exstr
|
||||
contentState.cmdline_filter = ""
|
||||
// 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
|
||||
if (exstr !== commandline_state.clInput.value) {
|
||||
contentState.cmdline_filter = exstr
|
||||
return
|
||||
}
|
||||
|
||||
onInputPromise = refresh_completions(exstr)
|
||||
onInputPromise.then(() => {
|
||||
contentState.cmdline_filter = exstr
|
||||
})
|
||||
}, 100)
|
||||
})
|
||||
|
||||
|
@ -276,9 +301,10 @@ async function history(n) {
|
|||
HISTORY_SEARCH_STRING = commandline_state.clInput.value
|
||||
}
|
||||
|
||||
const matches = (await State.getAsync("cmdHistory")).filter(key =>
|
||||
key.startsWith(HISTORY_SEARCH_STRING),
|
||||
)
|
||||
// Check for matches in history, removing duplicates
|
||||
const matches = R.reverse(
|
||||
R.uniq(R.reverse(await State.getAsync("cmdHistory"))),
|
||||
).filter(key => key.startsWith(HISTORY_SEARCH_STRING))
|
||||
if (commandline_state.cmdline_history_position === 0) {
|
||||
cmdline_history_current = commandline_state.clInput.value
|
||||
}
|
||||
|
@ -319,63 +345,13 @@ export function fillcmdline(
|
|||
return result
|
||||
}
|
||||
|
||||
/** @hidden
|
||||
* Create a temporary textarea and give it to fn. Remove the textarea afterwards
|
||||
*
|
||||
* Useful for document.execCommand
|
||||
**/
|
||||
function applyWithTmpTextArea(fn) {
|
||||
let textarea
|
||||
try {
|
||||
textarea = document.createElement("textarea")
|
||||
// Scratchpad must be `display`ed, but can be tiny and invisible.
|
||||
// Being tiny and invisible means it won't make the parent page move.
|
||||
textarea.style.cssText =
|
||||
"visible: invisible; width: 0; height: 0; position: fixed"
|
||||
textarea.contentEditable = "true"
|
||||
document.documentElement.appendChild(textarea)
|
||||
return fn(textarea)
|
||||
} finally {
|
||||
document.documentElement.removeChild(textarea)
|
||||
}
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
export async function setClipboard(content: string) {
|
||||
await Messaging.messageOwnTab("commandline_content", "focus")
|
||||
applyWithTmpTextArea(scratchpad => {
|
||||
scratchpad.value = content
|
||||
scratchpad.select()
|
||||
// This can return false spuriously so just ignore its return value
|
||||
document.execCommand("Copy")
|
||||
logger.info("set clipboard:", scratchpad.value)
|
||||
})
|
||||
// Return focus to the document
|
||||
await Messaging.messageOwnTab("commandline_content", "hide")
|
||||
return Messaging.messageOwnTab("commandline_content", "blur")
|
||||
}
|
||||
|
||||
/** @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
|
||||
await Messaging.messageOwnTab("commandline_content", "hide")
|
||||
await Messaging.messageOwnTab("commandline_content", "blur")
|
||||
return result
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
export function getContent() {
|
||||
return commandline_state.clInput.value
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
export function editor_function(fn_name, ...args) {
|
||||
export function editor_function(fn_name: keyof typeof tri_editor, ...args) {
|
||||
let result = Promise.resolve([])
|
||||
if (tri_editor[fn_name]) {
|
||||
tri_editor[fn_name](commandline_state.clInput, ...args)
|
||||
|
@ -388,12 +364,8 @@ export function editor_function(fn_name, ...args) {
|
|||
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"
|
||||
import { TabGroupCompletionSource } from "./completions/TabGroup"
|
||||
commandline_state.fns = getCommandlineFns(commandline_state)
|
||||
Messaging.addListener(
|
||||
"commandline_cmd",
|
||||
|
|
|
@ -15,6 +15,7 @@ import { enumerate } from "@src/lib/itertools"
|
|||
import { toNumber } from "@src/lib/convert"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
import { backoff } from "@src/lib/patience"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
export const DEFAULT_FAVICON = browser.runtime.getURL(
|
||||
"static/defaultFavicon.svg",
|
||||
|
@ -28,7 +29,7 @@ export abstract class CompletionOption {
|
|||
/** What to fill into cmdline */
|
||||
value: string
|
||||
/** Control presentation of the option */
|
||||
state: OptionState
|
||||
abstract state: OptionState
|
||||
}
|
||||
|
||||
export abstract class CompletionSource {
|
||||
|
@ -93,7 +94,7 @@ export abstract class CompletionSource {
|
|||
/** Update [[node]] to display completions relevant to exstr */
|
||||
public abstract filter(exstr: string): Promise<void>
|
||||
|
||||
abstract async next(inc?: number): Promise<boolean>
|
||||
abstract next(inc?: number): Promise<boolean>
|
||||
}
|
||||
|
||||
// Default classes
|
||||
|
@ -157,6 +158,11 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
keys: ["fuseKeys"],
|
||||
shouldSort: true,
|
||||
includeScore: true,
|
||||
findAllMatches: true,
|
||||
ignoreLocation: true,
|
||||
ignoreFieldNorm: true,
|
||||
threshold: config.get("completionfuzziness"),
|
||||
minMatchCharLength: 1,
|
||||
}
|
||||
|
||||
// PERF: Could be expensive not to cache Fuse()
|
||||
|
@ -241,7 +247,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
}
|
||||
|
||||
/** Rtn sorted array of {option, score} */
|
||||
scoredOptions(query: string, options = this.options): ScoredOption[] {
|
||||
scoredOptions(query: string): ScoredOption[] {
|
||||
const searchThis = this.options.map((elem, index) => ({
|
||||
index,
|
||||
fuseKeys: elem.fuseKeys,
|
||||
|
@ -290,28 +296,17 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
}
|
||||
|
||||
/** Call to replace the current display */
|
||||
// TODO: optionContainer.replaceWith and optionContainer.remove don't work.
|
||||
// I don't know why, but it means we can't replace the div in one go. Maybe
|
||||
// an iframe thing.
|
||||
updateDisplay() {
|
||||
/* const newContainer = html`<div>` */
|
||||
|
||||
while (this.optionContainer.hasChildNodes()) {
|
||||
this.optionContainer.removeChild(this.optionContainer.lastChild)
|
||||
}
|
||||
const newContainer = this.optionContainer.cloneNode(false) as HTMLElement
|
||||
|
||||
for (const option of this.options) {
|
||||
/* newContainer.appendChild(option.html) */
|
||||
if (option.state !== "hidden")
|
||||
this.optionContainer.appendChild(option.html)
|
||||
// This is probably slow: `.html` means the HTML parser will be invoked
|
||||
newContainer.appendChild(option.html)
|
||||
}
|
||||
this.optionContainer.replaceWith(newContainer)
|
||||
this.optionContainer = newContainer
|
||||
this.next(0)
|
||||
|
||||
/* console.log('updateDisplay', this.optionContainer, newContainer) */
|
||||
|
||||
/* let result1 = this.optionContainer.remove() */
|
||||
/* let res2 = this.node.appendChild(newContainer) */
|
||||
/* console.log('results', result1, res2) */
|
||||
}
|
||||
|
||||
async next(inc = 1) {
|
||||
|
@ -335,7 +330,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
/* abstract onUpdate(query: string, prefix: string, options: CompletionOptionFuse[]) */
|
||||
|
||||
// Lots of methods don't need this but some do
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars-experimental
|
||||
async onInput(exstr: string) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as providers from "@src/completions/providers"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
class BmarkCompletionOption extends Completions.CompletionOptionHTML
|
||||
class BmarkCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
||||
|
@ -31,11 +33,15 @@ class BmarkCompletionOption extends Completions.CompletionOptionHTML
|
|||
|
||||
export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options: BmarkCompletionOption[]
|
||||
private shouldSetStateFromScore = true
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["bmarks"], "BmarkCompletionSource", "Bookmarks")
|
||||
|
||||
this._parent.appendChild(this.node)
|
||||
this.sortScoredOptions = true
|
||||
this.shouldSetStateFromScore =
|
||||
config.get("completions", "Bmark", "autoselect") === "true"
|
||||
}
|
||||
|
||||
public async filter(exstr: string) {
|
||||
|
@ -70,12 +76,22 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
|
|||
.slice(0, 10)
|
||||
.map(page => new BmarkCompletionOption(option + page.url, page))
|
||||
|
||||
this.lastExstr = prefix + query
|
||||
return this.updateChain()
|
||||
}
|
||||
|
||||
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
|
||||
super.setStateFromScore(scoredOpts, this.shouldSetStateFromScore)
|
||||
}
|
||||
|
||||
updateChain() {
|
||||
// Options are pre-trimmed to the right length.
|
||||
this.options.forEach(option => (option.state = "normal"))
|
||||
const query = this.splitOnPrefix(this.lastExstr)[1]
|
||||
|
||||
if (query && query.trim().length > 0) {
|
||||
this.setStateFromScore(this.scoredOptions(query))
|
||||
} else {
|
||||
this.options.forEach(option => (option.state = "normal"))
|
||||
}
|
||||
|
||||
// Call concrete class
|
||||
return this.updateDisplay()
|
||||
|
|
|
@ -27,6 +27,7 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
|
|||
return this.updateOptions(exstr)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
|
||||
updateChain(exstr = this.lastExstr, options = this.options) {
|
||||
if (this.options.length > 0) this.state = "normal"
|
||||
else this.state = "hidden"
|
||||
|
|
|
@ -37,6 +37,7 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return this.updateOptions(exstr)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
|
||||
updateChain(exstr = this.lastExstr, options = this.options) {
|
||||
if (this.options.length > 0) this.state = "normal"
|
||||
else this.state = "hidden"
|
||||
|
|
|
@ -3,7 +3,8 @@ import * as Messaging from "@src/lib/messaging"
|
|||
import * as Completions from "../completions"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
class FindCompletionOption extends Completions.CompletionOptionHTML
|
||||
class FindCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
constructor(m, reverse = false) {
|
||||
|
@ -26,12 +27,10 @@ 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)
|
||||
}
|
||||
|
||||
|
@ -48,7 +47,7 @@ export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
|||
}
|
||||
|
||||
// Overriding this function is important, the default one has a tendency to hide options when you don't expect it
|
||||
setStateFromScore(scoredOpts, autoselect) {
|
||||
setStateFromScore() {
|
||||
this.options.forEach(o => (o.state = "normal"))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import * as Messaging from "@src/lib/messaging"
|
||||
import * as Completions from "@src/completions"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
class RssCompletionOption extends Completions.CompletionOptionHTML
|
||||
class RssCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
||||
|
@ -29,9 +31,15 @@ export class RssCompletionSource extends Completions.CompletionSourceFuse {
|
|||
super(["rssexec"], "RssCompletionSource", "Feeds")
|
||||
|
||||
this.updateOptions()
|
||||
this.shouldSetStateFromScore =
|
||||
config.get("completions", "Rss", "autoselect") === "true"
|
||||
this._parent.appendChild(this.node)
|
||||
}
|
||||
|
||||
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
|
||||
super.setStateFromScore(scoredOpts, this.shouldSetStateFromScore)
|
||||
}
|
||||
|
||||
onInput(...whatever) {
|
||||
return this.updateOptions(...whatever)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { browserBg } from "@src/lib/webext.ts"
|
||||
import { browserBg } from "@src/lib/webext"
|
||||
import * as Completions from "@src/completions"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
function computeDate(session) {
|
||||
let howLong = Math.round(
|
||||
((new Date() as any) - session.lastModified) / 1000,
|
||||
)
|
||||
let qualifier = "s"
|
||||
if (howLong > 60) {
|
||||
if (Math.abs(howLong) > 60) {
|
||||
qualifier = "m"
|
||||
howLong = Math.round(howLong / 60)
|
||||
if (howLong > 60) {
|
||||
if (Math.abs(howLong) > 60) {
|
||||
qualifier = "h"
|
||||
howLong = Math.round(howLong / 60)
|
||||
if (howLong > 24) {
|
||||
if (Math.abs(howLong) > 24) {
|
||||
qualifier = "d"
|
||||
howLong = Math.round(howLong / 24)
|
||||
}
|
||||
|
@ -41,7 +42,8 @@ function getTabInfo(session) {
|
|||
return [tab, extraInfo]
|
||||
}
|
||||
|
||||
class SessionCompletionOption extends Completions.CompletionOptionHTML
|
||||
class SessionCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
||||
|
@ -71,6 +73,8 @@ export class SessionsCompletionSource extends Completions.CompletionSourceFuse {
|
|||
super(["undo"], "SessionCompletionSource", "sessions")
|
||||
|
||||
this.updateOptions()
|
||||
this.shouldSetStateFromScore =
|
||||
config.get("completions", "Sessions", "autoselect") === "true"
|
||||
this._parent.appendChild(this.node)
|
||||
}
|
||||
|
||||
|
@ -78,6 +82,10 @@ export class SessionsCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return this.updateOptions(exstr)
|
||||
}
|
||||
|
||||
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
|
||||
super.setStateFromScore(scoredOpts, this.shouldSetStateFromScore)
|
||||
}
|
||||
|
||||
private async updateOptions(exstr = "") {
|
||||
this.lastExstr = exstr
|
||||
const [prefix] = this.splitOnPrefix(exstr)
|
||||
|
|
|
@ -2,7 +2,8 @@ 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
|
||||
class SettingsCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
||||
|
@ -25,7 +26,7 @@ export class SettingsCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
constructor(private _parent) {
|
||||
super(
|
||||
["set", "get", "unset", "seturl", "unseturl"],
|
||||
["set", "get", "unset", "seturl", "unseturl", "viewconfig"],
|
||||
"SettingsCompletionSource",
|
||||
"Settings",
|
||||
)
|
||||
|
@ -52,7 +53,10 @@ export class SettingsCompletionSource 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
|
||||
// copy pasting code is fun and good
|
||||
if (prefix === "seturl " || prefix === "unseturl ") {
|
||||
if ((prefix === "seturl " || prefix === "unseturl ") || (
|
||||
prefix === "viewconfig " &&
|
||||
(query.startsWith("--user") || query.startsWith("--default"))
|
||||
)) {
|
||||
const args = query.split(" ")
|
||||
options = args.slice(0, 1).join(" ")
|
||||
query = args.slice(1).join(" ")
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import * as Perf from "@src/perf"
|
||||
import { browserBg } from "@src/lib/webext.ts"
|
||||
import { browserBg } from "@src/lib/webext"
|
||||
import { enumerate } from "@src/lib/itertools"
|
||||
import * as Containers from "@src/lib/containers"
|
||||
import * as Completions from "@src/completions"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as Messaging from "@src/lib/messaging"
|
||||
import * as R from "ramda"
|
||||
|
||||
class BufferCompletionOption extends Completions.CompletionOptionHTML
|
||||
class BufferCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
public tabIndex: number
|
||||
|
@ -42,13 +42,14 @@ class BufferCompletionOption extends Completions.CompletionOptionHTML
|
|||
const favIconUrl = tab.favIconUrl
|
||||
? tab.favIconUrl
|
||||
: Completions.DEFAULT_FAVICON
|
||||
const indicator = tab.audible ? String.fromCodePoint(0x1f50a) : ""
|
||||
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="icon"><img loading="lazy" src="${favIconUrl}" /></td>
|
||||
<td class="title">${tab.index + 1}: ${indicator} ${tab.title}</td>
|
||||
<td class="content">
|
||||
<a class="url" target="_blank" href=${tab.url}>${tab.url}</a>
|
||||
</td>
|
||||
|
@ -67,15 +68,24 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
constructor(private _parent) {
|
||||
super(
|
||||
["tab", "tabclose", "tabdetach", "tabduplicate", "tabmove"],
|
||||
[
|
||||
"tab",
|
||||
"tabclose",
|
||||
"tabdetach",
|
||||
"tabduplicate",
|
||||
"tabmove",
|
||||
"tabrename",
|
||||
],
|
||||
"BufferCompletionSource",
|
||||
"Tabs",
|
||||
)
|
||||
this.sortScoredOptions = true
|
||||
this.shouldSetStateFromScore =
|
||||
config.get("completions", "Tab", "autoselect") === "true"
|
||||
this.updateOptions()
|
||||
this._parent.appendChild(this.node)
|
||||
|
||||
Messaging.addListener("tab_changes", (message) => this.reactToTabChanges(message.command))
|
||||
Messaging.addListener("tab_changes", () => this.reactToTabChanges())
|
||||
}
|
||||
|
||||
async onInput(exstr) {
|
||||
|
@ -86,6 +96,8 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
async filter(exstr) {
|
||||
this.lastExstr = exstr
|
||||
const prefix = this.splitOnPrefix(exstr).shift()
|
||||
if (prefix === "tabrename ") this.shouldSetStateFromScore = false
|
||||
return this.onInput(exstr)
|
||||
}
|
||||
|
||||
|
@ -124,25 +136,7 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
}
|
||||
|
||||
// If not yet returned...
|
||||
return super.scoredOptions(query, options)
|
||||
}
|
||||
|
||||
/** Return the scoredOption[] result for the nth tab */
|
||||
private nthTabscoredOptions(
|
||||
n: number,
|
||||
options: BufferCompletionOption[],
|
||||
): Completions.ScoredOption[] {
|
||||
for (const [index, option] of enumerate(options)) {
|
||||
if (option.tabIndex === n) {
|
||||
return [
|
||||
{
|
||||
index,
|
||||
option,
|
||||
score: 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
return super.scoredOptions(query)
|
||||
}
|
||||
|
||||
/** Return the scoredOption[] result for the tab index startswith n */
|
||||
|
@ -236,9 +230,8 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
}
|
||||
|
||||
// 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 " && /^[+-][0-9]+$/.exec(query)
|
||||
)
|
||||
if (prefix === "tabmove")
|
||||
this.shouldSetStateFromScore = !/^[+-][0-9]+$/.exec(query)
|
||||
|
||||
await this.fillOptions()
|
||||
this.completion = undefined
|
||||
|
@ -256,18 +249,30 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
* Update the list of possible tab options and select (focus on)
|
||||
* the appropriate option.
|
||||
*/
|
||||
private async reactToTabChanges(command: string): Promise<void> {
|
||||
private async reactToTabChanges(): Promise<void> {
|
||||
const prevOptions = this.options
|
||||
await this.updateOptions(this.lastExstr)
|
||||
|
||||
if (!prevOptions || !this.options || !this.lastFocused) return
|
||||
if (!prevOptions || !this.options || !this.lastFocused) return
|
||||
|
||||
// Determine which option to focus on
|
||||
const diff = R.differenceWith((x, y) => x.tabId === y.tabId, prevOptions, this.options)
|
||||
const lastFocusedTabCompletion = this.lastFocused as BufferCompletionOption
|
||||
const diff: BufferCompletionOption[] = []
|
||||
for (const prevOption of prevOptions) {
|
||||
if (
|
||||
!this.options.find(
|
||||
newOption => prevOption.tabId === newOption.tabId,
|
||||
)
|
||||
)
|
||||
diff.push(prevOption)
|
||||
}
|
||||
const lastFocusedTabCompletion = this
|
||||
.lastFocused as BufferCompletionOption
|
||||
|
||||
// If the focused option was removed then focus on the next option
|
||||
if (diff.length === 1 && diff[0].tabId === lastFocusedTabCompletion.tabId) {
|
||||
if (
|
||||
diff.length === 1 &&
|
||||
diff[0].tabId === lastFocusedTabCompletion.tabId
|
||||
) {
|
||||
this.select(this.getTheNextTabOption(lastFocusedTabCompletion))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,14 @@ 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"
|
||||
import * as Messaging from "@src/lib/messaging"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
class TabAllCompletionOption extends Completions.CompletionOptionHTML
|
||||
class TabAllCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
public tab: browser.tabs.Tab
|
||||
constructor(
|
||||
public value: string,
|
||||
tab: browser.tabs.Tab,
|
||||
|
@ -16,6 +20,7 @@ class TabAllCompletionOption extends Completions.CompletionOptionHTML
|
|||
super()
|
||||
this.value = `${winindex}.${tab.index + 1}`
|
||||
this.fuseKeys.push(this.value, tab.title, tab.url)
|
||||
this.tab = tab
|
||||
|
||||
// Create HTMLElement
|
||||
const favIconUrl = tab.favIconUrl
|
||||
|
@ -40,12 +45,17 @@ class TabAllCompletionOption extends Completions.CompletionOptionHTML
|
|||
|
||||
export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options: TabAllCompletionOption[]
|
||||
private shouldSetStateFromScore = true
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["taball", "tabgrab"], "TabAllCompletionSource", "All Tabs")
|
||||
|
||||
this.updateOptions()
|
||||
this._parent.appendChild(this.node)
|
||||
this.shouldSetStateFromScore =
|
||||
config.get("completions", "TabAll", "autoselect") === "true"
|
||||
|
||||
Messaging.addListener("tab_changes", () => this.reactToTabChanges())
|
||||
}
|
||||
|
||||
async onInput(exstr) {
|
||||
|
@ -53,7 +63,7 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
|||
}
|
||||
|
||||
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
|
||||
super.setStateFromScore(scoredOpts, true)
|
||||
super.setStateFromScore(scoredOpts, this.shouldSetStateFromScore)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,6 +76,48 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of possible tab options and select (focus on)
|
||||
* the appropriate option.
|
||||
*/
|
||||
private async reactToTabChanges(): Promise<void> {
|
||||
// const prevOptions = this.options
|
||||
await this.updateOptions(this.lastExstr)
|
||||
|
||||
// TODO: update this from Tab.ts for TabAll.ts
|
||||
// if (!prevOptions || !this.options || !this.lastFocused) return
|
||||
|
||||
// // Determine which option to focus on
|
||||
// const diff = R.differenceWith(
|
||||
// (x, y) => x.tab.id === y.tab.id,
|
||||
// prevOptions,
|
||||
// this.options,
|
||||
// )
|
||||
// const lastFocusedTabCompletion = this
|
||||
// .lastFocused as TabAllCompletionOption
|
||||
|
||||
// // If the focused option was removed then focus on the next option
|
||||
// if (
|
||||
// diff.length === 1 &&
|
||||
// diff[0].tab.id === lastFocusedTabCompletion.tab.id
|
||||
// ) {
|
||||
// //this.select(this.getTheNextTabOption(lastFocusedTabCompletion))
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next option in this BufferCompletionSource assuming
|
||||
* that this BufferCompletionSource length has been reduced by 1
|
||||
*
|
||||
* TODO: this ain't going to work, need to work out position based on win.tab
|
||||
*/
|
||||
// private getTheNextTabOption(option: TabAllCompletionOption) {
|
||||
// if (option.tab.index === this.options.length) {
|
||||
// return this.options[this.options.length - 1]
|
||||
// }
|
||||
// return this.options[option.tab.index]
|
||||
// }
|
||||
|
||||
// Eslint doesn't like this decorator but there's nothing we can do about it
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
@Perf.measuredAsync
|
||||
|
|
90
src/completions/Theme.ts
Normal file
90
src/completions/Theme.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import { staticThemes } from "@src/.metadata.generated"
|
||||
import * as config from "@src/lib/config"
|
||||
|
||||
export class ThemeCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
constructor(public value: string, public documentation: string = "") {
|
||||
super()
|
||||
this.fuseKeys.push(this.value)
|
||||
|
||||
// Create HTMLElement
|
||||
this.html = html`<tr class="ThemeCompletionOption option">
|
||||
<td class="theme">${value}</td>
|
||||
</tr>`
|
||||
}
|
||||
}
|
||||
|
||||
export class ThemeCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options: ThemeCompletionOption[]
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["set theme", "colourscheme"], "ThemeCompletionSource", "Themes")
|
||||
|
||||
this.updateOptions()
|
||||
this._parent.appendChild(this.node)
|
||||
}
|
||||
|
||||
async filter(exstr) {
|
||||
this.lastExstr = exstr
|
||||
return this.onInput(exstr)
|
||||
}
|
||||
|
||||
async onInput(exstr) {
|
||||
return this.updateOptions(exstr)
|
||||
}
|
||||
|
||||
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
|
||||
super.setStateFromScore(scoredOpts, false)
|
||||
}
|
||||
|
||||
private async updateOptions(exstr = "") {
|
||||
this.lastExstr = exstr
|
||||
|
||||
const themes = staticThemes.concat(
|
||||
Object.keys(await config.get("customthemes")),
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
|
||||
this.options = this.scoreOptions(
|
||||
themes
|
||||
.filter(name => name.startsWith(query))
|
||||
.map(name => new ThemeCompletionOption(name)),
|
||||
)
|
||||
|
||||
this.options.forEach(o => (o.state = "normal"))
|
||||
return this.updateChain()
|
||||
}
|
||||
|
||||
private scoreOptions(options: ThemeCompletionOption[]) {
|
||||
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()
|
||||
// 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
|
||||
// })
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { browserBg } from "@src/lib/webext.ts"
|
||||
import { browserBg } from "@src/lib/webext"
|
||||
import * as Completions from "@src/completions"
|
||||
|
||||
class WindowCompletionOption extends Completions.CompletionOptionHTML
|
||||
class WindowCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
||||
|
|
103
src/content.ts
103
src/content.ts
|
@ -13,7 +13,7 @@ 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 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!")
|
||||
|
@ -24,15 +24,49 @@ import {
|
|||
addContentStateChangedListener,
|
||||
} from "@src/content/state_content"
|
||||
|
||||
import { CmdlineCmds } from "@src/content/commandline_cmds"
|
||||
import { EditorCmds } from "@src/content/editor"
|
||||
|
||||
import { getAllDocumentFrames } from "@src/lib/dom"
|
||||
|
||||
import state from "@src/state"
|
||||
import { EditorCmds as editor } from "@src/content/editor"
|
||||
/* tslint:disable:import-spacing */
|
||||
|
||||
config.getAsync("superignore").then(async TRI_DISABLE => {
|
||||
// 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"
|
||||
|
||||
if (TRI_DISABLE === "true") return
|
||||
const controller = await import("@src/lib/controller")
|
||||
const excmds_content = await import("@src/.excmds_content.generated")
|
||||
const hinting_content = await import("@src/content/hinting")
|
||||
// Hook the keyboard up to the controller
|
||||
const ContentController = await import("@src/content/controller_content")
|
||||
// Add various useful modules to the window for debugging
|
||||
const commandline_content = await import("@src/content/commandline_content")
|
||||
const convert = await import("@src/lib/convert")
|
||||
const dom = await import("@src/lib/dom")
|
||||
const excmds = await import("@src/.excmds_content.generated")
|
||||
const finding_content = await import("@src/content/finding")
|
||||
const itertools = await import("@src/lib/itertools")
|
||||
const messaging = await import("@src/lib/messaging")
|
||||
const State = await import("@src/state")
|
||||
const webext = await import("@src/lib/webext")
|
||||
const perf = await import("@src/perf")
|
||||
const keyseq = await import("@src/lib/keyseq")
|
||||
const native = await import("@src/lib/native")
|
||||
const styling = await import("@src/content/styling")
|
||||
const updates = await import("@src/lib/updates")
|
||||
const urlutils = await import("@src/lib/url_util")
|
||||
const scrolling = await import("@src/content/scrolling")
|
||||
const R = await import("ramda")
|
||||
const visual = await import("@src/lib/visual")
|
||||
const metadata = await import("@src/.metadata.generated")
|
||||
const { tabTgroup } = await import("@src/lib/tab_groups")
|
||||
|
||||
controller.setExCmds({
|
||||
"": excmds_content,
|
||||
ex: CmdlineCmds,
|
||||
|
@ -51,10 +85,6 @@ messaging.addListener(
|
|||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
messaging.addListener("alive", async () => true)
|
||||
|
||||
// Hook the keyboard up to the controller
|
||||
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)
|
||||
|
@ -131,32 +161,6 @@ config.getAsync("preventautofocusjackhammer").then(allowautofocus => {
|
|||
}
|
||||
tryPreventAutoFocus()
|
||||
})
|
||||
|
||||
// Add various useful modules to the window for debugging
|
||||
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 State from "@src/state"
|
||||
import * as webext from "@src/lib/webext"
|
||||
import Mark from "mark.js"
|
||||
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"
|
||||
import * as urlutils from "@src/lib/url_util"
|
||||
import * as scrolling from "@src/content/scrolling"
|
||||
import * as R from "ramda"
|
||||
import * as visual from "@src/lib/visual"
|
||||
import { tabTgroup } from "./lib/tab_groups"
|
||||
/* tslint:disable:import-spacing */
|
||||
;(window as any).tri = Object.assign(Object.create(null), {
|
||||
browserBg: webext.browserBg,
|
||||
commandline_content,
|
||||
|
@ -170,7 +174,7 @@ import { tabTgroup } from "./lib/tab_groups"
|
|||
hinting_content,
|
||||
itertools,
|
||||
logger,
|
||||
Mark,
|
||||
metadata,
|
||||
keyseq,
|
||||
messaging,
|
||||
state,
|
||||
|
@ -206,9 +210,8 @@ if (
|
|||
if (newtab) {
|
||||
excmds.open_quiet(newtab)
|
||||
} else {
|
||||
document.body.style.height = "100%"
|
||||
document.body.style.opacity = "1"
|
||||
document.body.style.overflow = "auto"
|
||||
const content = document.getElementById("trinewtab")
|
||||
content.style.display = "block"
|
||||
document.title = "Tridactyl Top Tips & New Tab Page"
|
||||
}
|
||||
}
|
||||
|
@ -333,8 +336,7 @@ config.getAsync("modeindicator").then(mode => {
|
|||
} else {
|
||||
result = mode
|
||||
}
|
||||
|
||||
const modeindicatorshowkeys = Config.get("modeindicatorshowkeys")
|
||||
const modeindicatorshowkeys = config.get("modeindicatorshowkeys")
|
||||
if (modeindicatorshowkeys === "true" && suffix !== "") {
|
||||
result = mode + " " + suffix
|
||||
}
|
||||
|
@ -355,7 +357,12 @@ config.getAsync("modeindicator").then(mode => {
|
|||
statusIndicator.className +=
|
||||
" TridactylMode" + statusIndicator.textContent
|
||||
|
||||
if (config.get("modeindicator") !== "true") statusIndicator.remove()
|
||||
if (
|
||||
config.get("modeindicator") !== "true" ||
|
||||
config.get("modeindicatormodes", mode) === "false"
|
||||
) {
|
||||
statusIndicator.remove()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -385,6 +392,14 @@ config.getAsync("leavegithubalone").then(v => {
|
|||
}
|
||||
})
|
||||
|
||||
// I still don't get lib/messaging.ts
|
||||
const phoneHome = () => browser.runtime.sendMessage("dom_loaded_background")
|
||||
|
||||
document.readyState === "complete" && phoneHome()
|
||||
window.addEventListener("load", () => {
|
||||
phoneHome()
|
||||
})
|
||||
|
||||
document.addEventListener("selectionchange", () => {
|
||||
const selection = document.getSelection()
|
||||
if (
|
||||
|
@ -412,3 +427,5 @@ document.addEventListener("selectionchange", () => {
|
|||
;(window as any).tri = Object.assign(window.tri, {
|
||||
perfObserver: perf.listenForCounters(),
|
||||
})
|
||||
|
||||
}) // End of maybe-disable-tridactyl-a-bit wrapper
|
||||
|
|
|
@ -27,13 +27,15 @@ cmdline_iframe.setAttribute(
|
|||
browser.runtime.getURL("static/commandline.html"),
|
||||
)
|
||||
cmdline_iframe.setAttribute("id", "cmdline_iframe")
|
||||
cmdline_iframe.setAttribute("loading", "lazy")
|
||||
|
||||
let enabled = false
|
||||
|
||||
/** Initialise the cmdline_iframe element unless the window location is included in a value of config/noiframe */
|
||||
async function init() {
|
||||
const noiframe = await config.getAsync("noiframe")
|
||||
if (noiframe === "false" && !enabled) {
|
||||
const notridactyl = await config.getAsync("superignore")
|
||||
if (noiframe === "false" && notridactyl !== "true" && !enabled) {
|
||||
hide()
|
||||
document.documentElement.appendChild(cmdline_iframe)
|
||||
enabled = true
|
||||
|
@ -44,7 +46,7 @@ async function init() {
|
|||
|
||||
// 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.
|
||||
init().catch(e => {
|
||||
init().catch(() => {
|
||||
// Surrender event loop with setTimeout() to page JS in case it's still doing stuff.
|
||||
document.addEventListener("DOMContentLoaded", () =>
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -2,9 +2,9 @@ import {
|
|||
messageOwnTab,
|
||||
addListener,
|
||||
attributeCaller,
|
||||
} from "@src/lib/messaging.ts"
|
||||
} from "@src/lib/messaging"
|
||||
import * as DOM from "@src/lib/dom"
|
||||
import * as _EditorCmds from "@src/lib/editor.ts"
|
||||
import * as _EditorCmds from "@src/lib/editor"
|
||||
|
||||
export const EditorCmds = new Proxy(_EditorCmds, {
|
||||
get(target, property) {
|
||||
|
|
|
@ -30,7 +30,9 @@ class FindHighlight extends HTMLSpanElement {
|
|||
super()
|
||||
;(this as any).unfocus = () => {
|
||||
for (const node of this.children) {
|
||||
;(node as HTMLElement).style.background = `rgba(127,255,255,0.5)`
|
||||
;(
|
||||
node as HTMLElement
|
||||
).style.background = `rgba(127,255,255,0.5)`
|
||||
}
|
||||
}
|
||||
;(this as any).focus = () => {
|
||||
|
@ -48,7 +50,9 @@ class FindHighlight extends HTMLSpanElement {
|
|||
parentNode.focus()
|
||||
}
|
||||
for (const node of this.children) {
|
||||
;(node as HTMLElement).style.background = `rgba(255,127,255,0.5)`
|
||||
;(
|
||||
node as HTMLElement
|
||||
).style.background = `rgba(255,127,255,0.5)`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +85,14 @@ let lastHighlights
|
|||
// Which element of `lastSearch` was last selected
|
||||
let selected = 0
|
||||
|
||||
let HIGHLIGHT_TIMER
|
||||
|
||||
export async function jumpToMatch(searchQuery, reverse) {
|
||||
const timeout = config.get("findhighlighttimeout")
|
||||
if (timeout > 0) {
|
||||
clearTimeout(HIGHLIGHT_TIMER)
|
||||
HIGHLIGHT_TIMER = setTimeout(removeHighlighting, timeout)
|
||||
}
|
||||
// First, search for the query
|
||||
const findcase = config.get("findcase")
|
||||
const sensitive =
|
||||
|
@ -164,6 +175,11 @@ export async function jumpToNextMatch(n: number) {
|
|||
return lastSearchQuery ? jumpToMatch(lastSearchQuery, n < 0) : undefined
|
||||
}
|
||||
if (!host.firstChild) {
|
||||
const timeout = config.get("findhighlighttimeout")
|
||||
if (timeout > 0) {
|
||||
clearTimeout(HIGHLIGHT_TIMER)
|
||||
HIGHLIGHT_TIMER = setTimeout(removeHighlighting, timeout)
|
||||
}
|
||||
drawHighlights(lastHighlights)
|
||||
}
|
||||
if (lastHighlights[selected] === undefined) {
|
||||
|
|
|
@ -402,10 +402,11 @@ interface Hintables {
|
|||
export function hintPage(
|
||||
hintableElements: Hintables[],
|
||||
onSelect: HintSelectedCallback,
|
||||
resolve = () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
reject = () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
resolve: (x?) => void = () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
reject: (x?) => void = () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
rapid = false,
|
||||
) {
|
||||
reset() // Tidy up in case any previous hinting wasn't exited cleanly
|
||||
const buildHints: HintBuilder = defaultHintBuilder()
|
||||
const filterHints: HintFilter = defaultHintFilter()
|
||||
contentState.mode = "hint"
|
||||
|
@ -467,11 +468,12 @@ export function hintPage(
|
|||
)
|
||||
|
||||
if (
|
||||
modeState.hints.length == 1 ||
|
||||
(firstTargetIsSelectable() && allTargetsAreEqual())
|
||||
(modeState.hints.length == 1 ||
|
||||
(firstTargetIsSelectable() && allTargetsAreEqual())) &&
|
||||
config.get("hintautoselect") === "true"
|
||||
) {
|
||||
// There is just a single link or all the links point to the same
|
||||
// place. Select it.
|
||||
// place. Select it unless `hintautoselect` is set to `false`.
|
||||
modeState.cleanUpHints()
|
||||
modeState.hints[0].select()
|
||||
reset()
|
||||
|
@ -616,7 +618,7 @@ class Hint {
|
|||
public name: string,
|
||||
public readonly filterData: any,
|
||||
private readonly onSelect: HintSelectedCallback,
|
||||
private readonly classes?: string[],
|
||||
classes?: string[],
|
||||
) {
|
||||
// We need to compute the offset for elements that are in an iframe
|
||||
let offsetTop = 0
|
||||
|
@ -667,6 +669,10 @@ class Hint {
|
|||
this.hidden = false
|
||||
}
|
||||
|
||||
public static isHintable(target: Element): boolean {
|
||||
return target.getClientRects().length > 0
|
||||
}
|
||||
|
||||
setName(n: string) {
|
||||
this.name = n
|
||||
this.flag.textContent = this.name
|
||||
|
@ -748,7 +754,7 @@ function buildHintsSimple(
|
|||
hintables: Hintables,
|
||||
onSelect: HintSelectedCallback,
|
||||
) {
|
||||
const els = hintables.elements
|
||||
const els = hintables.elements.filter(el => Hint.isHintable(el))
|
||||
const names = Array.from(
|
||||
hintnames(els.length + modeState.hints.length),
|
||||
).slice(modeState.hints.length)
|
||||
|
@ -799,7 +805,7 @@ function buildHintsVimperator(
|
|||
hintables: Hintables,
|
||||
onSelect: HintSelectedCallback,
|
||||
) {
|
||||
const els = hintables.elements
|
||||
const els = hintables.elements.filter(el => Hint.isHintable(el))
|
||||
const names = Array.from(
|
||||
hintnames(els.length + modeState.hints.length),
|
||||
).slice(modeState.hints.length)
|
||||
|
@ -842,10 +848,10 @@ function filterHintsSimple(fstr) {
|
|||
|
||||
// Fix bug where sometimes a bigger number would be selected (e.g. 10 rather than 1)
|
||||
// such that smaller numbers couldn't be selected
|
||||
let hints = modeState.hints
|
||||
if (config.get("hintnames") == "numeric") {
|
||||
hints = R.sortBy(R.pipe(R.prop("name"), parseInt), modeState.hints)
|
||||
}
|
||||
const hints =
|
||||
config.get("hintnames") == "numeric"
|
||||
? R.sortBy(R.pipe(R.prop("name"), parseInt), modeState.hints)
|
||||
: modeState.hints
|
||||
|
||||
for (const h of hints) {
|
||||
if (!h.name.startsWith(fstr)) h.hidden = true
|
||||
|
@ -859,7 +865,7 @@ function filterHintsSimple(fstr) {
|
|||
active.push(h)
|
||||
}
|
||||
}
|
||||
if (active.length === 1) {
|
||||
if (active.length === 1 && config.get("hintautoselect") === "true") {
|
||||
selectFocusedHint()
|
||||
}
|
||||
}
|
||||
|
@ -945,8 +951,8 @@ function filterHintsVimperator(query: string, reflow = false) {
|
|||
modeState.focusedHint.focused = true
|
||||
}
|
||||
|
||||
// Select focused hint if it's the only match
|
||||
if (active.length === 1) {
|
||||
// Select focused hint if it's the only match unless turned off in config
|
||||
if (active.length === 1 && config.get("hintautoselect") === "true") {
|
||||
selectFocusedHint(true)
|
||||
}
|
||||
}
|
||||
|
@ -1000,29 +1006,29 @@ function pushSpace() {
|
|||
|
||||
Elements are hintable if
|
||||
1. they can be meaningfully selected, clicked, etc
|
||||
2. they're visible
|
||||
2. they're visible (unless includeInvisible is true)
|
||||
1. Within viewport
|
||||
2. Not hidden by another element
|
||||
|
||||
@hidden
|
||||
*/
|
||||
export function hintables(selectors = DOM.HINTTAGS_selectors, withjs = false) {
|
||||
const elems = R.pipe(
|
||||
DOM.getElemsBySelector,
|
||||
R.filter(DOM.isVisible),
|
||||
changeHintablesToLargestChild,
|
||||
)(selectors, [])
|
||||
export function hintables(
|
||||
selectors = DOM.HINTTAGS_selectors,
|
||||
withjs = false,
|
||||
includeInvisible = false,
|
||||
) {
|
||||
const visibleFilter = DOM.isVisibleFilter(includeInvisible)
|
||||
const elems = changeHintablesToLargestChild(
|
||||
DOM.getElemsBySelector(selectors, []).filter(visibleFilter),
|
||||
)
|
||||
const hintables: Hintables[] = [{ elements: elems }]
|
||||
if (withjs) {
|
||||
hintables.push({
|
||||
elements: R.pipe(
|
||||
Array.from,
|
||||
// Ramda gives an error here without the "any"
|
||||
// Problem for a rainy day :)
|
||||
R.filter(DOM.isVisible) as any,
|
||||
R.without(elems),
|
||||
changeHintablesToLargestChild,
|
||||
)(DOM.hintworthy_js_elems),
|
||||
elements: changeHintablesToLargestChild(
|
||||
Array.from(DOM.hintworthy_js_elems).filter(
|
||||
el => visibleFilter(el) && !elems.includes(el),
|
||||
),
|
||||
),
|
||||
hintclasses: ["TridactylJSHint"],
|
||||
})
|
||||
}
|
||||
|
@ -1074,15 +1080,19 @@ function isElementLargerThan(e1: Element, e2: Element): boolean {
|
|||
/** Returns elements that point to a saveable resource
|
||||
* @hidden
|
||||
*/
|
||||
export function saveableElements() {
|
||||
return DOM.getElemsBySelector(DOM.HINTTAGS_saveable, [DOM.isVisible])
|
||||
export function saveableElements(includeInvisible = false) {
|
||||
return DOM.getElemsBySelector(DOM.HINTTAGS_saveable, [
|
||||
DOM.isVisibleFilter(includeInvisible),
|
||||
])
|
||||
}
|
||||
|
||||
/** Get array of images in the viewport
|
||||
/** Get array of images in the viewport, or all images if includeInvisible is true
|
||||
* @hidden
|
||||
*/
|
||||
export function hintableImages() {
|
||||
return DOM.getElemsBySelector(DOM.HINTTAGS_img_selectors, [DOM.isVisible])
|
||||
export function hintableImages(includeInvisible = false) {
|
||||
return DOM.getElemsBySelector(DOM.HINTTAGS_img_selectors, [
|
||||
DOM.isVisibleFilter(includeInvisible),
|
||||
])
|
||||
}
|
||||
|
||||
/** Get array of selectable elements that display a text matching either plain
|
||||
|
@ -1092,68 +1102,51 @@ export function hintableImages() {
|
|||
export function hintByText(match: string | RegExp) {
|
||||
return DOM.getElemsBySelector(DOM.HINTTAGS_filter_by_text_selectors, [
|
||||
DOM.isVisible,
|
||||
hint => {
|
||||
let text
|
||||
if (hint instanceof HTMLInputElement) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
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())
|
||||
}
|
||||
},
|
||||
hintByTextFilter(match),
|
||||
])
|
||||
}
|
||||
|
||||
/** Return a predicate that checks whether an element matches a given text hinting filter
|
||||
* @hidden
|
||||
*/
|
||||
export function hintByTextFilter(match: string | RegExp): HintSelectedCallback {
|
||||
return hint => {
|
||||
let text
|
||||
if (hint instanceof HTMLInputElement) {
|
||||
text = hint.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() {
|
||||
export function killables(includeInvisible = false) {
|
||||
return DOM.getElemsBySelector(DOM.HINTTAGS_killable_selectors, [
|
||||
DOM.isVisible,
|
||||
DOM.isVisibleFilter(includeInvisible),
|
||||
])
|
||||
}
|
||||
|
||||
/** 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: Element[] | Hintables[] = DOM.elementsWithText(),
|
||||
action: HintSelectedCallback = _ => _,
|
||||
rapid = false,
|
||||
): Promise<[Element, number]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
hintPage(toHintablesArray(elements), action, resolve, reject, rapid)
|
||||
})
|
||||
}
|
||||
|
||||
// Multiple dispatch? who needs it
|
||||
function toHintablesArray(
|
||||
/** Returns an array of hintable objects from an array of elements
|
||||
* @hidden
|
||||
* */
|
||||
export function toHintablesArray(
|
||||
hintablesOrElements: Element[] | Hintables[],
|
||||
): Hintables[] {
|
||||
return "className" in hintablesOrElements[0]
|
||||
? [{ elements: hintablesOrElements } as Hintables]
|
||||
: "elements" in hintablesOrElements[0]
|
||||
? (hintablesOrElements as Hintables[])
|
||||
: undefined
|
||||
if (!hintablesOrElements.length) return []
|
||||
if ("className" in hintablesOrElements[0])
|
||||
return [{ elements: hintablesOrElements } as Hintables]
|
||||
if ("elements" in hintablesOrElements[0])
|
||||
return hintablesOrElements as Hintables[]
|
||||
return []
|
||||
}
|
||||
|
||||
function selectFocusedHint(delay = false) {
|
||||
|
@ -1205,7 +1198,7 @@ export function parser(keys: keyseq.KeyEventLike[]) {
|
|||
keyseq.mapstrMapToKeyMap(
|
||||
new Map(
|
||||
(Object.entries(config.get("hintmaps")) as any).filter(
|
||||
([key, value]) => value != "",
|
||||
([_key, value]) => value != "",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -180,7 +180,6 @@ export async function recursiveScroll(
|
|||
xDistance: number,
|
||||
yDistance: number,
|
||||
node?: Element,
|
||||
stopAt?: Element,
|
||||
) {
|
||||
let startingFromCached = false
|
||||
if (!node) {
|
||||
|
|
|
@ -20,8 +20,9 @@ export class PrevInput {
|
|||
class ContentState {
|
||||
mode: ModeName = "normal"
|
||||
suffix = ""
|
||||
// to trigger status indicator updates
|
||||
group: string = ""
|
||||
current_cmdline = ""
|
||||
cmdline_filter = ""
|
||||
}
|
||||
|
||||
export type ContentStateProperty =
|
||||
|
|
|
@ -28,9 +28,19 @@ const customCss = {
|
|||
|
||||
export async function theme(element) {
|
||||
// Remove any old theme
|
||||
|
||||
/**
|
||||
* DEPRECATED
|
||||
*
|
||||
* You don't need to add weird classnames to your themes any more, but you can if you want.
|
||||
*
|
||||
* Retained for backwards compatibility.
|
||||
**/
|
||||
for (const theme of THEMES.map(prefixTheme)) {
|
||||
element.classList.remove(theme)
|
||||
}
|
||||
// DEPRECATION ENDS
|
||||
|
||||
if (insertedCSS) {
|
||||
// Typescript doesn't seem to be aware than remove/insertCSS's tabid
|
||||
// argument is optional
|
||||
|
@ -40,14 +50,27 @@ export async function theme(element) {
|
|||
|
||||
const newTheme = await config.getAsync("theme")
|
||||
|
||||
// Add a class corresponding to config.get('theme')
|
||||
/**
|
||||
* DEPRECATED
|
||||
*
|
||||
* You don't need to add weird classnames to your themes any more, but you can if you want.
|
||||
*
|
||||
* Retained for backwards compatibility.
|
||||
**/
|
||||
if (newTheme !== "default") {
|
||||
element.classList.add(prefixTheme(newTheme))
|
||||
}
|
||||
// DEPRECATION ENDS
|
||||
|
||||
// Insert custom css if needed
|
||||
if (newTheme !== "default" && !THEMES.includes(newTheme)) {
|
||||
customCss.code = await config.getAsync("customthemes", newTheme)
|
||||
if (newTheme !== "default") {
|
||||
customCss.code = THEMES.includes(newTheme)
|
||||
? "@import url('" +
|
||||
browser.runtime.getURL(
|
||||
"static/themes/" + newTheme + "/" + newTheme + ".css",
|
||||
) +
|
||||
"');"
|
||||
: await config.getAsync("customthemes", newTheme)
|
||||
if (customCss.code) {
|
||||
await browserBg.tabs.insertCSS(await ownTabId(), customCss)
|
||||
insertedCSS = true
|
||||
|
@ -80,6 +103,13 @@ function retheme() {
|
|||
|
||||
config.addChangeListener("theme", retheme)
|
||||
|
||||
/**
|
||||
* DEPRECATED
|
||||
*
|
||||
* You don't need to add weird classnames to your themes any more, but you can if you want.
|
||||
*
|
||||
* Retained for backwards compatibility.
|
||||
**/
|
||||
// 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
|
||||
const cb = async mutationList => {
|
||||
|
@ -97,3 +127,4 @@ new MutationObserver(cb).observe(document.documentElement, {
|
|||
attributeOldValue: false,
|
||||
attributeFilter: ["class"],
|
||||
})
|
||||
// DEPRECATION ENDS
|
||||
|
|
|
@ -7,36 +7,138 @@
|
|||
*
|
||||
* 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 chinese = "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑".split("")
|
||||
const colour = "#0F0" // green text
|
||||
rain(chinese, colour)
|
||||
rain(makeBlock(), chinese, colour)
|
||||
}
|
||||
|
||||
export function music() {
|
||||
// music characters - taken from the unicode charset
|
||||
const music = "𝄞𝄟𝄰𝅘𝅥𝅮𝅘𝅥𝅯𝅘𝅥𝅰𝄽".split("")
|
||||
const colour = "#ead115"
|
||||
rain(makeBlock(), music, colour)
|
||||
}
|
||||
|
||||
export function no_mouse() {
|
||||
rain([" "], "#FFF", 0) // No characters, unused colour code, no darkening
|
||||
makeBlock()
|
||||
}
|
||||
|
||||
export const snow = () => rain(["❄"], "#FFF", 0.15)
|
||||
function makeBlock() {
|
||||
const overlaydiv = document.createElement("div")
|
||||
overlaydiv.className = "_tridactyl_no_mouse_"
|
||||
overlaydiv.style.position = "fixed"
|
||||
overlaydiv.style.display = "block"
|
||||
overlaydiv.style.width = String(window.innerWidth)
|
||||
overlaydiv.style.height = String(document.documentElement.scrollHeight)
|
||||
overlaydiv.style.top = "0px"
|
||||
overlaydiv.style.bottom = "0px"
|
||||
overlaydiv.style.left = "0px"
|
||||
overlaydiv.style.right = "0px"
|
||||
overlaydiv.style.zIndex = "1000"
|
||||
overlaydiv.style.opacity = "0.5"
|
||||
overlaydiv.style.cursor = "none"
|
||||
document.body.appendChild(overlaydiv)
|
||||
return overlaydiv
|
||||
}
|
||||
|
||||
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"
|
||||
export function drawable() {
|
||||
eraser = false
|
||||
make_drawable(makeBlock())
|
||||
}
|
||||
|
||||
const clickX = []
|
||||
const clickY = []
|
||||
const clickDrag = []
|
||||
let ink
|
||||
|
||||
let eraser = false;
|
||||
export function eraser_toggle() {
|
||||
eraser = !eraser
|
||||
}
|
||||
|
||||
function addClick(x, y, dragging) {
|
||||
clickX.push(x)
|
||||
clickY.push(y)
|
||||
clickDrag.push(dragging)
|
||||
}
|
||||
|
||||
function redraw(context) {
|
||||
if(eraser) {
|
||||
context.globalCompositeOperation = "destination-out"
|
||||
context.lineWidth = 18
|
||||
} else {
|
||||
context.globalCompositeOperation = "source-over"
|
||||
context.lineWidth = 3
|
||||
}
|
||||
context.strokeStyle = "#000000"
|
||||
context.lineJoin = "miter"
|
||||
for(let i=0; i < clickX.length; i++) {
|
||||
context.beginPath()
|
||||
if(clickDrag[i] && i){
|
||||
context.moveTo(clickX[i-1], clickY[i-1])
|
||||
} else {
|
||||
context.moveTo(clickX[i]-1, clickY[i])
|
||||
}
|
||||
context.lineTo(clickX[i], clickY[i])
|
||||
context.closePath()
|
||||
context.stroke()
|
||||
}
|
||||
}
|
||||
function handleDown(e, context){
|
||||
ink = true
|
||||
addClick(e.pageX, e.pageY, false)
|
||||
redraw(context)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
function handleUp(e){
|
||||
ink = false
|
||||
clickX.length = 0
|
||||
clickY.length = 0
|
||||
clickDrag.length = 0
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
function handleMove(e, context) {
|
||||
if(ink){
|
||||
addClick(e.pageX, e.pageY, true);
|
||||
redraw(context);
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
function make_drawable(overlaydiv) {
|
||||
overlaydiv.style.position = "absolute"
|
||||
overlaydiv.style.opacity = "0.8"
|
||||
const c = document.createElement("canvas")
|
||||
d.appendChild(c)
|
||||
document.body.appendChild(d)
|
||||
overlaydiv.appendChild(c)
|
||||
const context = c.getContext("2d")
|
||||
// making the canvas full screen
|
||||
c.height = document.documentElement.scrollHeight
|
||||
c.width = window.innerWidth*0.98 // workaround to fix canvas overflow
|
||||
c.style.touchAction = "none" // for pen tablet to work
|
||||
c.addEventListener("pointerdown", e=>handleDown(e,context))
|
||||
c.addEventListener("pointerup", handleUp)
|
||||
c.addEventListener("pointermove", e=>handleMove(e,context))
|
||||
}
|
||||
|
||||
export function removeBlock() {
|
||||
Array.from(document.getElementsByClassName("_tridactyl_no_mouse_")).map((el: Element & { intid?: number | null}) => {
|
||||
if(typeof el.intid === "number") {
|
||||
clearInterval(el.intid)
|
||||
}
|
||||
el.remove()
|
||||
})
|
||||
}
|
||||
|
||||
export const snow = () => rain(makeBlock(), ["❄"], "#FFF", 0.15)
|
||||
|
||||
function rain(overlaydiv, characters: string[], colour, darkening = 0.05) {
|
||||
const c = document.createElement("canvas")
|
||||
overlaydiv.appendChild(c)
|
||||
const ctx = c.getContext("2d")
|
||||
|
||||
// making the canvas full screen
|
||||
|
@ -79,6 +181,5 @@ export function rain(characters: string[], colour, darkening = 0.05) {
|
|||
drops[i]++
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(draw, 33)
|
||||
overlaydiv.intid = setInterval(draw, 33)
|
||||
}
|
||||
|
|
1677
src/excmds.ts
1677
src/excmds.ts
File diff suppressed because it is too large
Load diff
|
@ -90,7 +90,7 @@ async function addSetting(settingName: string) {
|
|||
}
|
||||
|
||||
async function onExcmdPageLoad() {
|
||||
browser.storage.onChanged.addListener((changes, areaname) => {
|
||||
browser.storage.onChanged.addListener((changes) => {
|
||||
if ("userconfig" in changes) {
|
||||
// JSON.stringify for comparisons like it's 2012
|
||||
;[...modeMaps, "exaliases"].forEach(kind => {
|
||||
|
|
578
src/lib/DANGEROUS-html-tagged-template.js
Normal file
578
src/lib/DANGEROUS-html-tagged-template.js
Normal file
|
@ -0,0 +1,578 @@
|
|||
(function(window) {
|
||||
"use strict"
|
||||
|
||||
// test for es6 support of needed functionality
|
||||
try {
|
||||
// spread operator and template strings support
|
||||
(function testSpreadOpAndTemplate() {
|
||||
const tag = function tag() {
|
||||
return
|
||||
}
|
||||
// We don't need this value - we're just checking if its attempted creation causes any errors
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
tag`test`
|
||||
})()
|
||||
|
||||
// template tag and Array.from support
|
||||
if (
|
||||
!(
|
||||
"content" in document.createElement("template") &&
|
||||
"from" in Array
|
||||
)
|
||||
) {
|
||||
throw new Error()
|
||||
}
|
||||
} catch (e) {
|
||||
// missing support;
|
||||
console.log(
|
||||
"Your browser does not support the needed functionality to use the html tagged template",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof window.html === "undefined") {
|
||||
// --------------------------------------------------
|
||||
// constants
|
||||
// --------------------------------------------------
|
||||
|
||||
const SUBSTITUTION_INDEX = "substitutionindex:" // tag names are always all lowercase
|
||||
const SUBSTITUTION_REGEX = new RegExp(
|
||||
SUBSTITUTION_INDEX + "([0-9]+):",
|
||||
"g",
|
||||
)
|
||||
|
||||
// rejection string is used to replace xss attacks that cannot be escaped either
|
||||
// because the escaped string is still executable
|
||||
// (e.g. setTimeout(/* escaped string */)) or because it produces invalid results
|
||||
// (e.g. <h${xss}> where xss='><script>alert(1337)</script')
|
||||
// @see https://developers.google.com/closure/templates/docs/security#in_tags_and_attrs
|
||||
const REJECTION_STRING = "zXssPreventedz"
|
||||
|
||||
// which characters should be encoded in which contexts
|
||||
const ENCODINGS = {
|
||||
attribute: {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
},
|
||||
uri: {
|
||||
"&": "&",
|
||||
},
|
||||
}
|
||||
|
||||
// which attributes are DOM Level 0 events
|
||||
// taken from https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0
|
||||
const DOM_EVENTS = [
|
||||
"onclick",
|
||||
"ondblclick",
|
||||
"onmousedown",
|
||||
"onmouseup",
|
||||
"onmouseover",
|
||||
"onmousemove",
|
||||
"onmouseout",
|
||||
"ondragstart",
|
||||
"ondrag",
|
||||
"ondragenter",
|
||||
"ondragleave",
|
||||
"ondragover",
|
||||
"ondrop",
|
||||
"ondragend",
|
||||
"onkeydown",
|
||||
"onkeypress",
|
||||
"onkeyup",
|
||||
"onload",
|
||||
"onunload",
|
||||
"onabort",
|
||||
"onerror",
|
||||
"onresize",
|
||||
"onscroll",
|
||||
"onselect",
|
||||
"onchange",
|
||||
"onsubmit",
|
||||
"onreset",
|
||||
"onfocus",
|
||||
"onblur",
|
||||
"onpointerdown",
|
||||
"onpointerup",
|
||||
"onpointercancel",
|
||||
"onpointermove",
|
||||
"onpointerover",
|
||||
"onpointerout",
|
||||
"onpointerenter",
|
||||
"onpointerleave",
|
||||
"ongotpointercapture",
|
||||
"onlostpointercapture",
|
||||
"oncut",
|
||||
"oncopy",
|
||||
"onpaste",
|
||||
"onbeforecut",
|
||||
"onbeforecopy",
|
||||
"onbeforepaste",
|
||||
"onafterupdate",
|
||||
"onbeforeupdate",
|
||||
"oncellchange",
|
||||
"ondataavailable",
|
||||
"ondatasetchanged",
|
||||
"ondatasetcomplete",
|
||||
"onerrorupdate",
|
||||
"onrowenter",
|
||||
"onrowexit",
|
||||
"onrowsdelete",
|
||||
"onrowinserted",
|
||||
"oncontextmenu",
|
||||
"ondrag",
|
||||
"ondragstart",
|
||||
"ondragenter",
|
||||
"ondragover",
|
||||
"ondragleave",
|
||||
"ondragend",
|
||||
"ondrop",
|
||||
"onselectstart",
|
||||
"help",
|
||||
"onbeforeunload",
|
||||
"onstop",
|
||||
"beforeeditfocus",
|
||||
"onstart",
|
||||
"onfinish",
|
||||
"onbounce",
|
||||
"onbeforeprint",
|
||||
"onafterprint",
|
||||
"onpropertychange",
|
||||
"onfilterchange",
|
||||
"onreadystatechange",
|
||||
"onlosecapture",
|
||||
"DOMMouseScroll",
|
||||
"ondragdrop",
|
||||
"ondragenter",
|
||||
"ondragexit",
|
||||
"ondraggesture",
|
||||
"ondragover",
|
||||
"onclose",
|
||||
"oncommand",
|
||||
"oninput",
|
||||
"DOMMenuItemActive",
|
||||
"DOMMenuItemInactive",
|
||||
"oncontextmenu",
|
||||
"onoverflow",
|
||||
"onoverflowchanged",
|
||||
"onunderflow",
|
||||
"onpopuphidden",
|
||||
"onpopuphiding",
|
||||
"onpopupshowing",
|
||||
"onpopupshown",
|
||||
"onbroadcast",
|
||||
"oncommandupdate",
|
||||
]
|
||||
|
||||
// which attributes take URIs
|
||||
// taken from https://www.w3.org/TR/html4/index/attributes.html
|
||||
const URI_ATTRIBUTES = [
|
||||
"action",
|
||||
"background",
|
||||
"cite",
|
||||
"classid",
|
||||
"codebase",
|
||||
"data",
|
||||
"href",
|
||||
"longdesc",
|
||||
"profile",
|
||||
"src",
|
||||
"usemap",
|
||||
]
|
||||
|
||||
const ENCODINGS_REGEX = {
|
||||
attribute: new RegExp(
|
||||
"[" + Object.keys(ENCODINGS.attribute).join("") + "]",
|
||||
"g",
|
||||
),
|
||||
uri: new RegExp(
|
||||
"[" + Object.keys(ENCODINGS.uri).join("") + "]",
|
||||
"g",
|
||||
),
|
||||
}
|
||||
|
||||
// find all attributes after the first whitespace (which would follow the tag
|
||||
// name. Only used when the DOM has been clobbered to still parse attributes
|
||||
const ATTRIBUTE_PARSER_REGEX = /\s([^">=\s]+)(?:="[^"]+")?/g
|
||||
|
||||
// test if a javascript substitution is wrapped with quotes
|
||||
const WRAPPED_WITH_QUOTES_REGEX = /^('|")[\s\S]*\1$/
|
||||
|
||||
// allow custom attribute names that start or end with url or ui to do uri escaping
|
||||
// @see https://developers.google.com/closure/templates/docs/security#in_urls
|
||||
const CUSTOM_URI_ATTRIBUTES_REGEX = /\bur[il]|ur[il]s?$/i
|
||||
|
||||
// --------------------------------------------------
|
||||
// private functions
|
||||
// --------------------------------------------------
|
||||
|
||||
/**
|
||||
* Escape HTML entities in an attribute.
|
||||
* @private
|
||||
*
|
||||
* @param {string} str - String to escape.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeAttributeHTMLEntities(str) {
|
||||
return str.replace(ENCODINGS_REGEX.attribute, function(match) {
|
||||
return ENCODINGS.attribute[match]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape entities in a URI.
|
||||
* @private
|
||||
*
|
||||
* @param {string} str - URI to escape.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeURIEntities(str) {
|
||||
return str.replace(ENCODINGS_REGEX.uri, function(match) {
|
||||
return ENCODINGS.uri[match]
|
||||
})
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// html tagged template function
|
||||
// --------------------------------------------------
|
||||
|
||||
/**
|
||||
* Safely convert a DOM string into DOM nodes using by using E4H and contextual
|
||||
* auto-escaping techniques to prevent xss attacks.
|
||||
*
|
||||
* @param {string[]} strings - Safe string literals.
|
||||
* @param {*} values - Unsafe substitution expressions.
|
||||
*
|
||||
* @returns {HTMLElement|DocumentFragment}
|
||||
*/
|
||||
window.html = function(strings, ...values) {
|
||||
// break early if called with empty content
|
||||
if (!strings[0] && values.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a string with substitution placeholders with its substitution values.
|
||||
* @private
|
||||
*
|
||||
* @param {string} match - Matched substitution placeholder.
|
||||
* @param {string} index - Substitution placeholder index.
|
||||
*/
|
||||
function replaceSubstitution(match, index) {
|
||||
return values[parseInt(index, 10)]
|
||||
}
|
||||
|
||||
// insert placeholders into the generated string so we can run it through the
|
||||
// HTML parser without any malicious content.
|
||||
// (this particular placeholder will even work when used to create a DOM element)
|
||||
let str = strings[0]
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
str += SUBSTITUTION_INDEX + i + ":" + strings[i + 1]
|
||||
}
|
||||
|
||||
// template tags allow any HTML (even <tr> elements out of context)
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
|
||||
const template = document.createElement("template")
|
||||
template.innerHTML = str
|
||||
|
||||
// find all substitution values and safely encode them using DOM APIs and
|
||||
// contextual auto-escaping
|
||||
const walker = document.createNodeIterator(
|
||||
template.content,
|
||||
NodeFilter.SHOW_ALL,
|
||||
)
|
||||
let node
|
||||
while ((node = walker.nextNode())) {
|
||||
let tag = null
|
||||
const attributesToRemove = []
|
||||
|
||||
// --------------------------------------------------
|
||||
// node name substitution
|
||||
// --------------------------------------------------
|
||||
|
||||
let nodeName = node.nodeName.toLowerCase()
|
||||
if (nodeName.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
||||
nodeName = nodeName.replace(
|
||||
SUBSTITUTION_REGEX,
|
||||
replaceSubstitution,
|
||||
)
|
||||
|
||||
// createElement() should not need to be escaped to prevent XSS?
|
||||
|
||||
// this will throw an error if the tag name is invalid (e.g. xss tried
|
||||
// to escape out of the tag using '><script>alert(1337)</script><')
|
||||
// instead of replacing the tag name we'll just let the error be thrown
|
||||
tag = document.createElement(nodeName)
|
||||
|
||||
// mark that this node needs to be cleaned up later with the newly
|
||||
// created node
|
||||
node._replacedWith = tag
|
||||
|
||||
// use insertBefore() instead of replaceChild() so that the node Iterator
|
||||
// doesn't think the new tag should be the next node
|
||||
node.parentNode.insertBefore(tag, node)
|
||||
|
||||
// special case for script tags:
|
||||
// using innerHTML with a string that contains a script tag causes the script
|
||||
// tag to not be executed when added to the DOM. We'll need to create a script
|
||||
// tag and append its contents which will make it execute correctly.
|
||||
// @see http://stackoverflow.com/questions/1197575/can-scripts-be-inserted-with-innerhtml
|
||||
} else if (node.nodeName === "SCRIPT") {
|
||||
const script = document.createElement("script")
|
||||
tag = script
|
||||
|
||||
node._replacedWith = script
|
||||
node.parentNode.insertBefore(script, node)
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// attribute substitution
|
||||
// --------------------------------------------------
|
||||
|
||||
let attributes
|
||||
if (node.attributes) {
|
||||
// if the attributes property is not of type NamedNodeMap then the DOM
|
||||
// has been clobbered. E.g. <form><input name="attributes"></form>.
|
||||
// We'll manually build up an array of objects that mimic the Attr
|
||||
// object so the loop will still work as expected.
|
||||
if (!(node.attributes instanceof NamedNodeMap)) {
|
||||
// first clone the node so we can isolate it from any children
|
||||
const temp = node.cloneNode()
|
||||
|
||||
// parse the node string for all attributes
|
||||
const attributeMatches = temp.outerHTML.match(
|
||||
ATTRIBUTE_PARSER_REGEX,
|
||||
)
|
||||
|
||||
// get all attribute names and their value
|
||||
attributes = []
|
||||
for (const attribute of attributeMatches.length) {
|
||||
const attributeName = attribute
|
||||
.trim()
|
||||
.split("=")[0]
|
||||
const attributeValue = node.getAttribute(
|
||||
attributeName,
|
||||
)
|
||||
|
||||
attributes.push({
|
||||
name: attributeName,
|
||||
value: attributeValue,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Windows 10 Firefox 44 will shift the attributes NamedNodeMap and
|
||||
// push the attribute to the end when using setAttribute(). We'll have
|
||||
// to clone the NamedNodeMap so the order isn't changed for setAttribute()
|
||||
attributes = Array.from(node.attributes)
|
||||
}
|
||||
|
||||
for (const attribute of attributes) {
|
||||
let name = attribute.name
|
||||
let value = attribute.value
|
||||
let hasSubstitution = false
|
||||
|
||||
// name has substitution
|
||||
if (name.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
||||
name = name.replace(
|
||||
SUBSTITUTION_REGEX,
|
||||
replaceSubstitution,
|
||||
)
|
||||
|
||||
// ensure substitution was with a non-empty string
|
||||
if (name && typeof name === "string") {
|
||||
hasSubstitution = true
|
||||
}
|
||||
|
||||
// remove old attribute
|
||||
attributesToRemove.push(attribute.name)
|
||||
}
|
||||
|
||||
// value has substitution - only check if name exists (only happens
|
||||
// when name is a substitution with an empty value)
|
||||
if (name && value.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
||||
hasSubstitution = true
|
||||
|
||||
// if an uri attribute has been rejected
|
||||
let isRejected = false
|
||||
|
||||
value = value.replace(SUBSTITUTION_REGEX, function(
|
||||
match,
|
||||
index,
|
||||
offset,
|
||||
) {
|
||||
if (isRejected) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let substitutionValue =
|
||||
values[parseInt(index, 10)]
|
||||
|
||||
// contextual auto-escaping:
|
||||
// if attribute is a DOM Level 0 event then we need to ensure it
|
||||
// is quoted
|
||||
if (
|
||||
DOM_EVENTS.indexOf(name) !== -1 &&
|
||||
typeof substitutionValue === "string" &&
|
||||
!WRAPPED_WITH_QUOTES_REGEX.test(
|
||||
substitutionValue,
|
||||
)
|
||||
) {
|
||||
substitutionValue =
|
||||
'"' + substitutionValue + '"'
|
||||
|
||||
// contextual auto-escaping:
|
||||
// if the attribute is a uri attribute then we need to uri encode it and
|
||||
// remove bad protocols
|
||||
} else if (
|
||||
URI_ATTRIBUTES.indexOf(name) !== -1 ||
|
||||
CUSTOM_URI_ATTRIBUTES_REGEX.test(name)
|
||||
) {
|
||||
// percent encode if the value is inside of a query parameter
|
||||
const queryParamIndex = value.indexOf("=")
|
||||
if (
|
||||
queryParamIndex !== -1 &&
|
||||
offset > queryParamIndex
|
||||
) {
|
||||
substitutionValue = encodeURIComponent(
|
||||
substitutionValue,
|
||||
)
|
||||
|
||||
// entity encode if value is part of the URL
|
||||
} else {
|
||||
substitutionValue = encodeURI(
|
||||
encodeURIEntities(
|
||||
substitutionValue,
|
||||
),
|
||||
)
|
||||
|
||||
// only allow the : when used after http or https otherwise reject
|
||||
// the entire url (will not allow any 'javascript:' or filter
|
||||
// evasion techniques)
|
||||
if (
|
||||
offset === 0 &&
|
||||
substitutionValue.indexOf(":") !==
|
||||
-1
|
||||
) {
|
||||
const authorized_protocols = [
|
||||
"http://",
|
||||
"https://",
|
||||
"moz-extension://",
|
||||
"about://",
|
||||
"data:image/png;base64",
|
||||
"data:image/gif;base64",
|
||||
"data:image/jpg;base64",
|
||||
"data:image/jpeg;base64",
|
||||
"data:image/x-icon;base64",
|
||||
// NB: THIS V COULD BE DANGEROUS
|
||||
"data:image/svg+xml;base64",
|
||||
// NB: THIS ^ COULD BE DANGEROUS
|
||||
]
|
||||
// If substitutionValue doesn't start with any of the authorized protocols
|
||||
if (
|
||||
!authorized_protocols.find(p =>
|
||||
substitutionValue.startsWith(
|
||||
p,
|
||||
),
|
||||
)
|
||||
) {
|
||||
isRejected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// contextual auto-escaping:
|
||||
// HTML encode attribute value if it is not a URL or URI to prevent
|
||||
// DOM Level 0 event handlers from executing xss code
|
||||
} else if (
|
||||
typeof substitutionValue === "string"
|
||||
) {
|
||||
substitutionValue = encodeAttributeHTMLEntities(
|
||||
substitutionValue,
|
||||
)
|
||||
}
|
||||
|
||||
return substitutionValue
|
||||
})
|
||||
|
||||
if (isRejected) {
|
||||
value = "#" + REJECTION_STRING
|
||||
}
|
||||
}
|
||||
|
||||
// add the attribute to the new tag or replace it on the current node
|
||||
// setAttribute() does not need to be escaped to prevent XSS since it does
|
||||
// all of that for us
|
||||
// @see https://www.mediawiki.org/wiki/DOM-based_XSS
|
||||
if (tag || hasSubstitution) {
|
||||
const el = tag || node
|
||||
|
||||
// optional attribute
|
||||
if (name.substr(-1) === "?") {
|
||||
el.removeAttribute(name)
|
||||
|
||||
if (value === "true") {
|
||||
name = name.slice(0, -1)
|
||||
el.setAttribute(name, "")
|
||||
}
|
||||
} else {
|
||||
el.setAttribute(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove placeholder attributes outside of the attribute loop since it
|
||||
// will modify the attributes NamedNodeMap indices.
|
||||
// @see https://github.com/straker/html-tagged-template/issues/13
|
||||
attributesToRemove.forEach(function(attribute) {
|
||||
node.removeAttribute(attribute)
|
||||
})
|
||||
|
||||
// append the current node to a replaced parent
|
||||
let parentNode
|
||||
if (node.parentNode && node.parentNode._replacedWith) {
|
||||
parentNode = node.parentNode
|
||||
node.parentNode._replacedWith.appendChild(node)
|
||||
}
|
||||
|
||||
// remove the old node from the DOM
|
||||
if (
|
||||
(node._replacedWith && node.childNodes.length === 0) ||
|
||||
(parentNode && parentNode.childNodes.length === 0)
|
||||
) {
|
||||
(parentNode || node).remove()
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// text content substitution
|
||||
// --------------------------------------------------
|
||||
|
||||
if (
|
||||
node.nodeType === 3 &&
|
||||
node.nodeValue.indexOf(SUBSTITUTION_INDEX) !== -1
|
||||
) {
|
||||
const nodeValue = node.nodeValue.replace(
|
||||
SUBSTITUTION_REGEX,
|
||||
replaceSubstitution,
|
||||
)
|
||||
|
||||
// createTextNode() should not need to be escaped to prevent XSS?
|
||||
const text = document.createTextNode(nodeValue)
|
||||
|
||||
// since the parent node has already gone through the iterator, we can use
|
||||
// replaceChild() here
|
||||
node.parentNode.replaceChild(text, node)
|
||||
}
|
||||
}
|
||||
|
||||
// return the documentFragment for multiple nodes
|
||||
if (template.content.childNodes.length > 1) {
|
||||
return template.content
|
||||
}
|
||||
|
||||
return template.content.firstChild
|
||||
}
|
||||
}
|
||||
})(window)
|
|
@ -284,7 +284,7 @@ export class AutoContain implements IAutoContain {
|
|||
} else {
|
||||
const containerExists = await Container.exists(aucons[aukeyarr[0]])
|
||||
if (!containerExists) {
|
||||
if (Config.get("auconcreatecontainer")) {
|
||||
if (Config.get("auconcreatecontainer") === "true") {
|
||||
await Container.create(aucons[aukeyarr[0]])
|
||||
} else {
|
||||
logger.error(
|
||||
|
|
|
@ -12,6 +12,7 @@ export const mode2maps = new Map([
|
|||
["ex", "exmaps"],
|
||||
["hint", "hintmaps"],
|
||||
["visual", "vmaps"],
|
||||
["browser", "browsermaps"],
|
||||
])
|
||||
|
||||
export const maps2mode = new Map(
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
import { messageOwnTab } from "@src/lib/messaging"
|
||||
import * as State from "@src/state"
|
||||
import { contentState } from "@src/content/state_content"
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
async function awaitProxyEq(proxy, a: string, b: string) {
|
||||
let counter = 0
|
||||
while (proxy[a] != proxy[b] && counter < 50) {
|
||||
await sleep(10)
|
||||
counter += 1
|
||||
}
|
||||
return proxy[a] == proxy[b]
|
||||
}
|
||||
|
||||
// One day we'll use typeof commandline_state from commandline_frame.ts
|
||||
export function getCommandlineFns(cmdline_state: {
|
||||
|
@ -26,7 +38,12 @@ export function getCommandlineFns(cmdline_state: {
|
|||
/**
|
||||
* Selects the next completion.
|
||||
*/
|
||||
next_completion: () => {
|
||||
next_completion: async () => {
|
||||
await awaitProxyEq(
|
||||
contentState,
|
||||
"current_cmdline",
|
||||
"cmdline_filter",
|
||||
)
|
||||
if (cmdline_state.activeCompletions)
|
||||
cmdline_state.activeCompletions.forEach(comp => comp.next())
|
||||
},
|
||||
|
@ -34,7 +51,12 @@ export function getCommandlineFns(cmdline_state: {
|
|||
/**
|
||||
* Selects the previous completion.
|
||||
*/
|
||||
prev_completion: () => {
|
||||
prev_completion: async () => {
|
||||
await awaitProxyEq(
|
||||
contentState,
|
||||
"current_cmdline",
|
||||
"cmdline_filter",
|
||||
)
|
||||
if (cmdline_state.activeCompletions)
|
||||
cmdline_state.activeCompletions.forEach(comp => comp.prev())
|
||||
},
|
||||
|
@ -50,7 +72,12 @@ export function getCommandlineFns(cmdline_state: {
|
|||
/**
|
||||
* Inserts the currently selected completion and a space in the command line.
|
||||
*/
|
||||
insert_completion: () => {
|
||||
insert_completion: async () => {
|
||||
await awaitProxyEq(
|
||||
contentState,
|
||||
"current_cmdline",
|
||||
"cmdline_filter",
|
||||
)
|
||||
const command = cmdline_state.getCompletion()
|
||||
if (cmdline_state.activeCompletions) {
|
||||
cmdline_state.activeCompletions.forEach(
|
||||
|
@ -157,7 +184,12 @@ export function getCommandlineFns(cmdline_state: {
|
|||
/**
|
||||
* Execute the content of the command line and hide it.
|
||||
**/
|
||||
accept_line: () => {
|
||||
accept_line: async () => {
|
||||
await awaitProxyEq(
|
||||
contentState,
|
||||
"current_cmdline",
|
||||
"cmdline_filter",
|
||||
)
|
||||
const command =
|
||||
cmdline_state.getCompletion() || cmdline_state.clInput.value
|
||||
|
||||
|
@ -179,9 +211,11 @@ export function getCommandlineFns(cmdline_state: {
|
|||
return messageOwnTab("controller_content", "acceptExCmd", [command])
|
||||
},
|
||||
|
||||
execute_ex_on_completion_args: (excmd: string) => execute_ex_on_x(true, cmdline_state, excmd),
|
||||
execute_ex_on_completion_args: (excmd: string) =>
|
||||
execute_ex_on_x(true, cmdline_state, excmd),
|
||||
|
||||
execute_ex_on_completion: (excmd: string) => execute_ex_on_x(false, cmdline_state, excmd),
|
||||
execute_ex_on_completion: (excmd: string) =>
|
||||
execute_ex_on_x(false, cmdline_state, excmd),
|
||||
|
||||
copy_completion: () => {
|
||||
const command = cmdline_state.getCompletion()
|
||||
|
@ -193,13 +227,12 @@ export function getCommandlineFns(cmdline_state: {
|
|||
}
|
||||
}
|
||||
|
||||
function execute_ex_on_x(args_only: boolean, cmdline_state, excmd: string){
|
||||
const args = cmdline_state.getCompletion(args_only) || cmdline_state.clInput.value
|
||||
function execute_ex_on_x(args_only: boolean, cmdline_state, excmd: string) {
|
||||
const args =
|
||||
cmdline_state.getCompletion(args_only) || cmdline_state.clInput.value
|
||||
|
||||
const cmdToExec = (excmd ? excmd : "") + " " + args
|
||||
const cmdToExec = (excmd ? excmd + " " : "") + args
|
||||
cmdline_state.fns.store_ex_string(cmdToExec)
|
||||
|
||||
return messageOwnTab("controller_content", "acceptExCmd", [
|
||||
cmdToExec,
|
||||
])
|
||||
return messageOwnTab("controller_content", "acceptExCmd", [cmdToExec])
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import * as R from "ramda"
|
||||
import * as binding from "@src/lib/binding"
|
||||
import * as platform from "@src/lib/platform"
|
||||
import { DeepPartial } from "tsdef"
|
||||
|
||||
/* Remove all nulls from objects recursively
|
||||
* NB: also applies to arrays
|
||||
|
@ -37,7 +38,7 @@ const CONFIGNAME = "userconfig"
|
|||
/** @hidden */
|
||||
const WAITERS = []
|
||||
/** @hidden */
|
||||
let INITIALISED = false
|
||||
export let INITIALISED = false
|
||||
|
||||
/** @hidden */
|
||||
// make a naked object
|
||||
|
@ -78,13 +79,34 @@ export class default_config {
|
|||
/**
|
||||
* Internal field to handle site-specific configs. Use :seturl/:unseturl to change these values.
|
||||
*/
|
||||
subconfigs: { [key: string]: default_config } = {
|
||||
subconfigs: { [key: string]: DeepPartial<default_config> } = {
|
||||
"www.google.com": {
|
||||
followpagepatterns: {
|
||||
next: "Next",
|
||||
prev: "Previous",
|
||||
},
|
||||
} as default_config,
|
||||
},
|
||||
"^https://web.whatsapp.com": {
|
||||
nmaps: {
|
||||
f: "hint -c [tabindex]:not(.two)>div,a",
|
||||
F: "hint -bc [tabindex]:not(.two)>div,a",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal field to handle mode-specific configs. Use :setmode/:unsetmode to change these values.
|
||||
*
|
||||
* Changing this might do weird stuff.
|
||||
*/
|
||||
modesubconfigs: { [key: string]: DeepPartial<default_config> } = {
|
||||
"normal": {},
|
||||
"insert": {},
|
||||
"input": {},
|
||||
"ignore": {},
|
||||
"ex": {},
|
||||
"hint": {},
|
||||
"visual": {},
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,13 +133,11 @@ export class default_config {
|
|||
|
||||
"<A-b>": "text.backward_word",
|
||||
"<A-f>": "text.forward_word",
|
||||
"<C-a>": "text.beginning_of_line",
|
||||
"<C-e>": "text.end_of_line",
|
||||
"<A-d>": "text.kill_word",
|
||||
"<S-Backspace>": "text.backward_kill_word",
|
||||
"<C-u>": "text.backward_kill_line",
|
||||
"<C-k>": "text.kill_line",
|
||||
"<C-c>": "text.kill_whole_line",
|
||||
|
||||
"<C-f>": "ex.complete",
|
||||
"<Tab>": "ex.next_completion",
|
||||
|
@ -176,6 +196,15 @@ export class default_config {
|
|||
"🕷🕷INHERITS🕷🕷": "imaps",
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable Tridactyl almost completely within a page, e.g. `seturl ^https?://mail.google.com disable true`. Only takes affect on page reload.
|
||||
*
|
||||
* You are usually better off using `blacklistadd` and `seturl [url] noiframe true` as you can then still use some Tridactyl binds, e.g. `shift-insert` for exiting ignore mode.
|
||||
*
|
||||
* NB: you should only use this with `seturl`. If you get trapped with Tridactyl disabled everywhere just run `tri unset superignore` in the Firefox address bar. If that still doesn't fix things, you can totally reset Tridactyl by running `tri help superignore` in the Firefox address bar, scrolling to the bottom of that page and then clicking "Reset Tridactyl config".
|
||||
*/
|
||||
superignore: "true" | "false" = "false"
|
||||
|
||||
/**
|
||||
* nmaps contain all of the bindings for "normal mode".
|
||||
*
|
||||
|
@ -201,6 +230,7 @@ export class default_config {
|
|||
ys: "clipboard yankshort",
|
||||
yc: "clipboard yankcanon",
|
||||
ym: "clipboard yankmd",
|
||||
yo: "clipboard yankorg",
|
||||
yt: "clipboard yanktitle",
|
||||
gh: "home",
|
||||
gH: "home true",
|
||||
|
@ -239,6 +269,7 @@ export class default_config {
|
|||
x: "stop",
|
||||
gi: "focusinput -l",
|
||||
"g?": "rot13",
|
||||
"g!": "jumble",
|
||||
"g;": "changelistjump -1",
|
||||
J: "tabprev",
|
||||
K: "tabnext",
|
||||
|
@ -249,6 +280,7 @@ export class default_config {
|
|||
"g^": "tabfirst",
|
||||
g0: "tabfirst",
|
||||
g$: "tablast",
|
||||
ga: "tabaudio",
|
||||
gr: "reader",
|
||||
gu: "urlparent",
|
||||
gU: "urlroot",
|
||||
|
@ -274,7 +306,9 @@ export class default_config {
|
|||
";o": "hint",
|
||||
";I": "hint -I",
|
||||
";k": "hint -k",
|
||||
";K": "hint -K",
|
||||
";y": "hint -y",
|
||||
";Y": "hint -cF img i => tri.excmds.yankimage(tri.urlutils.getAbsoluteURL(i.src))",
|
||||
";p": "hint -p",
|
||||
";h": "hint -h",
|
||||
v: "hint -h", // Easiest way of entering visual mode for now. Expect this bind to change
|
||||
|
@ -287,16 +321,15 @@ export class default_config {
|
|||
";;": "hint -; *",
|
||||
";#": "hint -#",
|
||||
";v": "hint -W mpvsafe",
|
||||
";V": "hint -V",
|
||||
";w": "hint -w",
|
||||
";t": "hint -W tabopen",
|
||||
";O": "hint -W fillcmdline_notrail open ",
|
||||
";W": "hint -W fillcmdline_notrail winopen ",
|
||||
";T": "hint -W fillcmdline_notrail tabopen ",
|
||||
";z": "hint -z",
|
||||
";m":
|
||||
"composite hint -pipe img src | js -p tri.excmds.open('images.google.com/searchbyimage?image_url=' + JS_ARG)",
|
||||
";M":
|
||||
"composite hint -pipe img src | jsb -p tri.excmds.tabopen('images.google.com/searchbyimage?image_url=' + JS_ARG)",
|
||||
";m": "composite hint -Jpipe img src | open images.google.com/searchbyimage?image_url=",
|
||||
";M": "composite hint -Jpipe img src | tabopen images.google.com/searchbyimage?image_url=",
|
||||
";gi": "hint -qi",
|
||||
";gI": "hint -qI",
|
||||
";gk": "hint -qk",
|
||||
|
@ -345,26 +378,19 @@ export class default_config {
|
|||
"<C-[>":
|
||||
"composite js document.getSelection().empty(); mode normal ; hidecmdline",
|
||||
y: "composite js document.getSelection().toString() | clipboard yank",
|
||||
s:
|
||||
"composite js document.getSelection().toString() | fillcmdline open search",
|
||||
S:
|
||||
"composite js document.getSelection().toString() | fillcmdline tabopen search",
|
||||
s: "composite js document.getSelection().toString() | fillcmdline open search",
|
||||
S: "composite js document.getSelection().toString() | fillcmdline tabopen search",
|
||||
l: 'js document.getSelection().modify("extend","forward","character")',
|
||||
h: 'js document.getSelection().modify("extend","backward","character")',
|
||||
e: 'js document.getSelection().modify("extend","forward","word")',
|
||||
w:
|
||||
'js document.getSelection().modify("extend","forward","word"); document.getSelection().modify("extend","forward","character")',
|
||||
b:
|
||||
'js document.getSelection().modify("extend","backward","character"); document.getSelection().modify("extend","backward","word"); document.getSelection().modify("extend","forward","character")',
|
||||
w: 'js document.getSelection().modify("extend","forward","word"); document.getSelection().modify("extend","forward","word"); document.getSelection().modify("extend","backward","word"); document.getSelection().modify("extend","forward","character")',
|
||||
b: 'js document.getSelection().modify("extend","backward","character"); document.getSelection().modify("extend","backward","word"); document.getSelection().modify("extend","forward","character")',
|
||||
j: 'js document.getSelection().modify("extend","forward","line")',
|
||||
// "j": 'js document.getSelection().modify("extend","forward","paragraph")', // not implemented in Firefox
|
||||
k: 'js document.getSelection().modify("extend","backward","line")',
|
||||
$:
|
||||
'js document.getSelection().modify("extend","forward","lineboundary")',
|
||||
"0":
|
||||
'js document.getSelection().modify("extend","backward","lineboundary")',
|
||||
"=":
|
||||
"js let n = document.getSelection().anchorNode.parentNode; let s = window.getSelection(); let r = document.createRange(); s.removeAllRanges(); r.selectNodeContents(n); s.addRange(r)",
|
||||
$: 'js document.getSelection().modify("extend","forward","lineboundary")',
|
||||
"0": 'js document.getSelection().modify("extend","backward","lineboundary")',
|
||||
"=": "js let n = document.getSelection().anchorNode.parentNode; let s = window.getSelection(); let r = document.createRange(); s.removeAllRanges(); r.selectNodeContents(n); s.addRange(r)",
|
||||
o: "js tri.visual.reverseSelection(document.getSelection())",
|
||||
"🕷🕷INHERITS🕷🕷": "nmaps",
|
||||
}
|
||||
|
@ -577,6 +603,7 @@ export class default_config {
|
|||
q: "tabclose",
|
||||
qa: "qall",
|
||||
sanitize: "sanitise",
|
||||
"saveas!": "saveas --cleanup --overwrite",
|
||||
tutorial: "tutor",
|
||||
h: "help",
|
||||
unmute: "mute unmute",
|
||||
|
@ -590,8 +617,7 @@ export class default_config {
|
|||
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",
|
||||
"!jsb": "fillcmdline_tmp 3000 !jsb is deprecated. Please use jsb instead",
|
||||
get_current_url: "js document.location.href",
|
||||
current_url: "composite get_current_url | fillcmdline_notrail ",
|
||||
stop: "js window.stop()",
|
||||
|
@ -602,7 +628,8 @@ export class default_config {
|
|||
"mkt!": "mktridactylrc -f",
|
||||
"mktridactylrc!": "mktridactylrc -f",
|
||||
mpvsafe:
|
||||
"js -p tri.excmds.shellescape(JS_ARG).then(url => tri.excmds.exclaim_quiet('mpv ' + url))",
|
||||
"js -p tri.excmds.shellescape(JS_ARG).then(url => tri.excmds.exclaim_quiet('mpv --no-terminal ' + url))",
|
||||
drawingstop: "no_mouse_mode",
|
||||
exto: "extoptions",
|
||||
extpreferences: "extoptions",
|
||||
extp: "extpreferences",
|
||||
|
@ -610,6 +637,7 @@ export class default_config {
|
|||
prefremove: "removepref",
|
||||
tabclosealltoright: "tabcloseallto right",
|
||||
tabclosealltoleft: "tabcloseallto left",
|
||||
reibadailty: "jumble",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -649,8 +677,7 @@ export class default_config {
|
|||
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=",
|
||||
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:
|
||||
|
@ -668,7 +695,7 @@ export class default_config {
|
|||
/**
|
||||
* 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`.
|
||||
* 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` or a `file:///` URI. You should instead use a data URI - https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs - or host a local webserver (e.g. Caddy).
|
||||
*/
|
||||
newtab = ""
|
||||
|
||||
|
@ -725,6 +752,13 @@ export class default_config {
|
|||
*/
|
||||
hintshift: "true" | "false" = "false"
|
||||
|
||||
/**
|
||||
* Controls whether hints should be followed automatically.
|
||||
*
|
||||
* If set to `false`, hints will only be followed upon confirmation. This applies to cases when there is only a single match or only one link on the page.
|
||||
*/
|
||||
hintautoselect: "true" | "false" = "true"
|
||||
|
||||
/**
|
||||
* Controls whether the page can focus elements for you via js
|
||||
*
|
||||
|
@ -752,7 +786,12 @@ export class default_config {
|
|||
/**
|
||||
* 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"
|
||||
tabopenpos: "next" | "last" | "related" = "next"
|
||||
|
||||
/**
|
||||
* When enabled (the default), running tabclose will close the tabs whether they are pinned or not. When disabled, tabclose will fail with an error if a tab is pinned.
|
||||
*/
|
||||
tabclosepinned: "true" | "false" = "true"
|
||||
|
||||
/**
|
||||
* Controls which tab order to use when opening the tab/buffer list. Either mru = sort by most recent tab or default = by tab index
|
||||
|
@ -781,9 +820,9 @@ export class default_config {
|
|||
ttspitch = 1
|
||||
|
||||
/**
|
||||
* If nextinput, <Tab> after gi brings selects the next input
|
||||
* When set to "nextinput", pressing `<Tab>` after gi selects the next input.
|
||||
*
|
||||
* If firefox, <Tab> selects the next selectable element, e.g. a link
|
||||
* When set to "firefox", `<Tab>` behaves like normal, focusing the next tab-indexed element regardless of type.
|
||||
*/
|
||||
gimode: "nextinput" | "firefox" = "nextinput"
|
||||
|
||||
|
@ -811,6 +850,19 @@ export class default_config {
|
|||
*/
|
||||
modeindicator: "true" | "false" = "true"
|
||||
|
||||
/**
|
||||
* Whether to display the mode indicator in various modes. Ignored if modeindicator set to false.
|
||||
*/
|
||||
modeindicatormodes: { [key: string]: "true" | "false" } = {
|
||||
normal: "true",
|
||||
insert: "true",
|
||||
input: "true",
|
||||
ignore: "true",
|
||||
ex: "true",
|
||||
hint: "true",
|
||||
visual: "true",
|
||||
}
|
||||
|
||||
/**
|
||||
* Milliseconds before registering a scroll in the jumplist
|
||||
*/
|
||||
|
@ -830,6 +882,7 @@ export class default_config {
|
|||
performance: "warning",
|
||||
state: "warning",
|
||||
styling: "warning",
|
||||
autocmds: "warning",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -915,16 +968,11 @@ export class default_config {
|
|||
* Set this to something weird if you want to have fun every time Tridactyl tries to update its native messenger.
|
||||
*
|
||||
* %TAG will be replaced with your version of Tridactyl for stable builds, or "master" for beta builds
|
||||
*
|
||||
* NB: Windows has its own platform-specific default.
|
||||
*/
|
||||
nativeinstallcmd =
|
||||
"curl -fsSl https://raw.githubusercontent.com/tridactyl/tridactyl/master/native/install.sh -o /tmp/trinativeinstall.sh && bash /tmp/trinativeinstall.sh %TAG"
|
||||
|
||||
/**
|
||||
* Set this to something weird if you want to have fun every time Tridactyl tries to update its native messenger.
|
||||
*
|
||||
* Replaces %WINTAG with "-Tag $TRI_VERSION", similarly to [[nativeinstallcmd]].
|
||||
*/
|
||||
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'))"`
|
||||
"curl -fsSl https://raw.githubusercontent.com/tridactyl/native_messenger/master/installers/install.sh -o /tmp/trinativeinstall.sh && sh /tmp/trinativeinstall.sh %TAG"
|
||||
|
||||
/**
|
||||
* Used by :updatecheck and related built-in functionality to automatically check for updates and prompt users to upgrade.
|
||||
|
@ -998,6 +1046,30 @@ export class default_config {
|
|||
*/
|
||||
bmarkweight = 100
|
||||
|
||||
/**
|
||||
* General completions options - NB: options are set according to our internal completion source name - see - `src/completions/[name].ts` in the Tridactyl source.
|
||||
*/
|
||||
completions = {
|
||||
Tab: {
|
||||
/**
|
||||
* Whether to automatically select the closest matching completion
|
||||
*/
|
||||
autoselect: "true",
|
||||
},
|
||||
TabAll: {
|
||||
autoselect: "true",
|
||||
},
|
||||
Rss: {
|
||||
autoselect: "true",
|
||||
},
|
||||
Bmark: {
|
||||
autoselect: "true",
|
||||
},
|
||||
Sessions: {
|
||||
autoselect: "true",
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of results that should be shown in completions. -1 for unlimited
|
||||
*/
|
||||
|
@ -1013,6 +1085,11 @@ export class default_config {
|
|||
*/
|
||||
findcase: "smart" | "sensitive" | "insensitive" = "smart"
|
||||
|
||||
/**
|
||||
* How long find highlights should persist in milliseconds. `<= 0` means they persist until cleared
|
||||
*/
|
||||
findhighlighttimeout = 0
|
||||
|
||||
/**
|
||||
* Whether Tridactyl should jump to the first match when using `:find`
|
||||
*/
|
||||
|
@ -1081,6 +1158,22 @@ export class default_config {
|
|||
* Whether to return to visual mode when text is deselected.
|
||||
*/
|
||||
visualexitauto: "true" | "false" = "true"
|
||||
|
||||
/**
|
||||
* Whether to open and close the sidebar quickly to get focus back to the page when <C-,> is pressed.
|
||||
*
|
||||
* Disable if the fact that it closes TreeStyleTabs gets on your nerves too much : )
|
||||
*
|
||||
* NB: when disabled, <C-,> can't get focus back from the address bar, but it can still get it back from lots of other places (e.g. Flash-style video players)
|
||||
*/
|
||||
escapehatchsidebarhack: "true" | "false" = "true"
|
||||
|
||||
/**
|
||||
* Threshold for fuzzy matching on completions. Lower => stricter matching. Range between 0 and 1: 0 corresponds to perfect matches only. 1 will match anything.
|
||||
*
|
||||
* https://fusejs.io/api/options.html#threshold
|
||||
*/
|
||||
completionfuzziness = 0.3
|
||||
}
|
||||
|
||||
const platform_defaults = {
|
||||
|
@ -1101,6 +1194,18 @@ const platform_defaults = {
|
|||
ignoremaps: {
|
||||
"<C-6>": "buffer #",
|
||||
} as unknown,
|
||||
|
||||
nativeinstallcmd: `powershell -ExecutionPolicy Bypass -NoProfile -Command "\
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12;\
|
||||
(New-Object System.Net.WebClient).DownloadFile('https://raw.githubusercontent.com/tridactyl/native_messenger/master/installers/windows.ps1', '%TEMP%/tridactyl_installnative.ps1');\
|
||||
& '%TEMP%/tridactyl_installnative.ps1' -Tag %TAG;\
|
||||
Remove-Item '%TEMP%/tridactyl_installnative.ps1'"`,
|
||||
},
|
||||
linux: {
|
||||
nmaps: {
|
||||
";x": 'hint -F e => { const pos = tri.dom.getAbsoluteCentre(e); tri.excmds.exclaim_quiet("xdotool mousemove --sync " + window.devicePixelRatio * pos.x + " " + window.devicePixelRatio * pos.y + "; xdotool click 1")}',
|
||||
";X": 'hint -F e => { const pos = tri.dom.getAbsoluteCentre(e); tri.excmds.exclaim_quiet("xdotool mousemove --sync " + window.devicePixelRatio * pos.x + " " + window.devicePixelRatio * pos.y + "; xdotool keydown ctrl+shift; xdotool click 1; xdotool keyup ctrl+shift")}',
|
||||
} as unknown,
|
||||
},
|
||||
} as Record<browser.runtime.PlatformOs, default_config>
|
||||
|
||||
|
@ -1120,7 +1225,7 @@ export const DEFAULTS = mergeDeepCull(
|
|||
@param target path of properties as an array
|
||||
@hidden
|
||||
*/
|
||||
function getDeepProperty(obj, target: string[]) {
|
||||
export function getDeepProperty(obj, target: string[]) {
|
||||
if (obj !== undefined && obj !== null && target.length) {
|
||||
if (obj["🕷🕷INHERITS🕷🕷"] === undefined) {
|
||||
return getDeepProperty(obj[target[0]], target.slice(1))
|
||||
|
@ -1296,9 +1401,14 @@ export async function getAsync(
|
|||
|
||||
/*
|
||||
* Replaces the configuration in your sync storage with your current configuration. Does not merge: it overwrites.
|
||||
*
|
||||
* Does not synchronise custom themes due to storage constraints.
|
||||
*/
|
||||
export async function push() {
|
||||
return browser.storage.sync.set(await browser.storage.local.get(CONFIGNAME))
|
||||
const local_conf = await browser.storage.local.get(CONFIGNAME)
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation
|
||||
delete local_conf[CONFIGNAME]["customthemes"]
|
||||
return browser.storage.sync.set(local_conf)
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -1312,7 +1422,14 @@ export async function pull() {
|
|||
* Like set(), but for a specific pattern.
|
||||
*/
|
||||
export function setURL(pattern, ...args) {
|
||||
return set("subconfigs", pattern, ...args)
|
||||
try {
|
||||
new RegExp(pattern)
|
||||
return set("subconfigs", pattern, ...args)
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError)
|
||||
throw new SyntaxError(`invalid pattern: ${err.message}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
/** Full target specification, then value
|
||||
|
||||
|
@ -1390,7 +1507,7 @@ export async function update() {
|
|||
if (subconfigs) {
|
||||
Object.keys(subconfigs)
|
||||
.map(pattern => [pattern, getURL(pattern, setting)])
|
||||
.filter(([pattern, value]) => value)
|
||||
.filter(([_pattern, value]) => value)
|
||||
.forEach(([pattern, value]) =>
|
||||
setURL(pattern, ...setting, fn(value)),
|
||||
)
|
||||
|
@ -1448,7 +1565,7 @@ export async function update() {
|
|||
getDeepProperty(USERCONFIG, [mapname]),
|
||||
])
|
||||
// mapobj is undefined if the user didn't define any bindings
|
||||
.filter(([mapname, mapobj]) => mapobj)
|
||||
.filter(([_mapname, mapobj]) => mapobj)
|
||||
.forEach(([mapname, mapobj]) => {
|
||||
// For each mapping
|
||||
Object.keys(mapobj)
|
||||
|
@ -1525,13 +1642,9 @@ export async function update() {
|
|||
})
|
||||
return mapObj
|
||||
}
|
||||
;[
|
||||
"nmaps",
|
||||
"exmaps",
|
||||
"imaps",
|
||||
"inputmaps",
|
||||
"ignoremaps",
|
||||
].forEach(settingName => updateAll([settingName], updateSetting))
|
||||
;["nmaps", "exmaps", "imaps", "inputmaps", "ignoremaps"].forEach(
|
||||
settingName => updateAll([settingName], updateSetting),
|
||||
)
|
||||
set("configversion", "1.7")
|
||||
}
|
||||
case "1.7": {
|
||||
|
@ -1766,6 +1879,9 @@ const parseConfigHelper = (pconf, parseobj, prefix = []) => {
|
|||
if (pconf[i][e] === 3) level = "info"
|
||||
if (pconf[i][e] === 4) level = "debug"
|
||||
parseobj.logging.push(`set logging.${e} ${level}`)
|
||||
} else if (i === "customthemes") {
|
||||
// Skip custom themes for now because writing their CSS is hard
|
||||
// parseobj.themes.push(`colourscheme ${e}`) // TODO: check if userconfig.theme == e and write this, otherwise don't.
|
||||
} else {
|
||||
parseConfigHelper(pconf[i], parseobj, [...prefix, i])
|
||||
break
|
||||
|
|
|
@ -168,20 +168,14 @@ export async function getAll(): Promise<any[]> {
|
|||
@returns The cookieStoreId of the first match of the query.
|
||||
*/
|
||||
export async function getId(name: string): Promise<string> {
|
||||
try {
|
||||
const containers = await getAll()
|
||||
const res = containers.filter(
|
||||
c => c.name.toLowerCase() === name.toLowerCase(),
|
||||
)
|
||||
if (res.length !== 1) {
|
||||
throw new Error("")
|
||||
} else {
|
||||
return res[0].cookieStoreId
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"[Container.getId] could not find a container with that name.",
|
||||
)
|
||||
const containers = await getAll()
|
||||
const res = containers.filter(
|
||||
c => c.name.toLowerCase() === name.toLowerCase(),
|
||||
)
|
||||
if (res.length !== 1) {
|
||||
throw new Error(`Container '${name}' does not exist.`)
|
||||
} else {
|
||||
return res[0].cookieStoreId
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as config from "@src/lib/config"
|
|||
import state from "@src/state"
|
||||
import * as State from "@src/state"
|
||||
import * as Logging from "@src/lib/logging"
|
||||
import { contentState } from "@src/content/state_content"
|
||||
import {
|
||||
activeTabId,
|
||||
openInNewTab,
|
||||
|
@ -119,8 +120,11 @@ export function mouseEvent(
|
|||
})
|
||||
}
|
||||
|
||||
export function elementsWithText() {
|
||||
return getElemsBySelector("*", [isVisible, hint => hint.textContent !== ""])
|
||||
export function elementsWithText(includeInvisible = false) {
|
||||
return getElemsBySelector("*", [
|
||||
isVisibleFilter(includeInvisible),
|
||||
hint => hint.textContent !== "",
|
||||
])
|
||||
}
|
||||
|
||||
/** Iterable of elements that match xpath.
|
||||
|
@ -202,6 +206,12 @@ export function widthMatters(style: CSSStyleDeclaration) {
|
|||
return true
|
||||
}
|
||||
|
||||
export function isVisibleFilter(
|
||||
includeInvisible: boolean,
|
||||
): (_: Element) => boolean {
|
||||
return (elem: Element) => includeInvisible || isVisible(elem)
|
||||
}
|
||||
|
||||
// Saka-key caches getComputedStyle. Maybe it's a good idea!
|
||||
/* let cgetComputedStyle = cacheDecorator(getComputedStyle) */
|
||||
|
||||
|
@ -524,11 +534,12 @@ export function getLastUsedInput(): HTMLElement {
|
|||
* https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1406825
|
||||
* */
|
||||
function onPageFocus(elem: HTMLElement, args: any[]): boolean {
|
||||
function onPageFocus(elem: HTMLElement): boolean {
|
||||
if (isTextEditable(elem)) {
|
||||
LAST_USED_INPUT = elem
|
||||
}
|
||||
return config.get("allowautofocus") === "true"
|
||||
const setting = config.get("modesubconfigs", contentState.mode, "allowautofocus") || config.get("allowautofocus")
|
||||
return setting === "true"
|
||||
}
|
||||
|
||||
async function setInput(el) {
|
||||
|
@ -575,12 +586,13 @@ export const HINTTAGS_selectors = `
|
|||
input:not([type=hidden]):not([disabled]),
|
||||
a,
|
||||
area,
|
||||
iframe,
|
||||
textarea,
|
||||
button,
|
||||
details,
|
||||
iframe,
|
||||
label,
|
||||
select,
|
||||
summary,
|
||||
label,
|
||||
textarea,
|
||||
[onclick],
|
||||
[onmouseover],
|
||||
[onmousedown],
|
||||
|
@ -650,8 +662,10 @@ export const HINTTAGS_saveable = `
|
|||
/** Get array of "anchors": elements which have id or name and can be addressed
|
||||
* with the hash/fragment in the URL
|
||||
*/
|
||||
export function anchors() {
|
||||
return getElemsBySelector(HINTTAGS_anchor_selectors, [isVisible])
|
||||
export function anchors(includeInvisible = false) {
|
||||
return getElemsBySelector(HINTTAGS_anchor_selectors, [
|
||||
isVisibleFilter(includeInvisible),
|
||||
])
|
||||
}
|
||||
|
||||
/** if `target === _blank` clicking the link is treated as opening a popup and is blocked. Use webext API to avoid that. */
|
||||
|
@ -678,6 +692,9 @@ export function simulateClick(target: HTMLElement) {
|
|||
})
|
||||
})
|
||||
} else {
|
||||
if (target instanceof HTMLDetailsElement) {
|
||||
target.open = !target.open
|
||||
}
|
||||
mouseEvent(target, "click")
|
||||
// DOM.focus has additional logic for focusing inputs
|
||||
focus(target)
|
||||
|
@ -685,12 +702,24 @@ export function simulateClick(target: HTMLElement) {
|
|||
}
|
||||
|
||||
/** Recursively resolves an active shadow DOM element. */
|
||||
export function deepestShadowRoot(sr: ShadowRoot|null): ShadowRoot|null {
|
||||
if (sr === null) return sr
|
||||
let shadowRoot = sr
|
||||
while (shadowRoot.activeElement.shadowRoot != null) {
|
||||
shadowRoot = shadowRoot.activeElement.shadowRoot
|
||||
}
|
||||
return shadowRoot
|
||||
export function deepestShadowRoot(sr: ShadowRoot | null): ShadowRoot | null {
|
||||
if (sr === null) return sr
|
||||
let shadowRoot = sr
|
||||
while (shadowRoot.activeElement.shadowRoot != null) {
|
||||
shadowRoot = shadowRoot.activeElement.shadowRoot
|
||||
}
|
||||
return shadowRoot
|
||||
}
|
||||
|
||||
export function getElementCentre(el) {
|
||||
const pos = el.getBoundingClientRect()
|
||||
return { x: 0.5 * (pos.left + pos.right), y: 0.5 * (pos.top + pos.bottom) }
|
||||
}
|
||||
|
||||
export function getAbsoluteCentre(el) {
|
||||
const pos = getElementCentre(el)
|
||||
return {
|
||||
x: pos.x + (window as any).mozInnerScreenX,
|
||||
y: pos.y + (window as any).mozInnerScreenY,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
getWordBoundaries,
|
||||
wordAfterPos,
|
||||
rot13_helper,
|
||||
jumble_helper
|
||||
} from "@src/lib/editor_utils"
|
||||
|
||||
/**
|
||||
|
@ -86,7 +87,7 @@ export const tab_insert = wrap_input((text, selectionStart, selectionEnd) => {
|
|||
* Behaves like readline's [transpose_chars](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC14), i.e. transposes the character to the left of the caret with the character to the right of the caret and then moves the caret one character to the right. If there are no characters to the right or to the left of the caret, uses the two characters the closest to the caret.
|
||||
**/
|
||||
export const transpose_chars = wrap_input(
|
||||
(text, selectionStart, selectionEnd) => {
|
||||
(text, selectionStart) => {
|
||||
if (text.length < 2) return [null, null, null]
|
||||
// When at the beginning of the text, transpose the first and second characters
|
||||
if (selectionStart === 0) selectionStart = 1
|
||||
|
@ -107,7 +108,7 @@ export const transpose_chars = wrap_input(
|
|||
* Applies a function to the word the caret is in, or to the next word if the caret is not in a word, or to the previous word if the current word is empty.
|
||||
*/
|
||||
function applyWord(
|
||||
text,
|
||||
text: string,
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
fn: (s: string) => string,
|
||||
|
@ -130,7 +131,7 @@ function applyWord(
|
|||
* Behaves like readline's [transpose_words](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC14). Basically equivalent to [[im_transpose_chars]], but using words as defined by the wordpattern setting.
|
||||
**/
|
||||
export const transpose_words = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
if (selectionStart >= text.length) {
|
||||
selectionStart = text.length - 1
|
||||
}
|
||||
|
@ -206,7 +207,7 @@ export const capitalize_word = wrap_input(
|
|||
* Behaves like readline's [kill_line](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC15), i.e. deletes every character to the right of the caret until reaching either the end of the text or the newline character (\n).
|
||||
**/
|
||||
export const kill_line = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
let newLine = text.substring(selectionStart).search("\n")
|
||||
if (newLine !== -1) {
|
||||
// If the caret is right before the newline, kill the newline
|
||||
|
@ -225,7 +226,7 @@ export const kill_line = wrap_input(
|
|||
* Behaves like readline's [backward_kill_line](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC15), i.e. deletes every character to the left of the caret until either the beginning of the text is found or a newline character ("\n") is reached.
|
||||
**/
|
||||
export const backward_kill_line = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
// If the caret is at the beginning of a line, join the lines
|
||||
if (selectionStart > 0 && text[selectionStart - 1] === "\n") {
|
||||
return [
|
||||
|
@ -255,7 +256,7 @@ export const backward_kill_line = wrap_input(
|
|||
* Behaves like readline's [kill_whole_line](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC15). Deletes every character between the two newlines the caret is in. If a newline can't be found on the left of the caret, everything is deleted until the beginning of the text is reached. If a newline can't be found on the right, everything is deleted until the end of the text is found.
|
||||
**/
|
||||
export const kill_whole_line = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
let firstNewLine
|
||||
let secondNewLine
|
||||
// Find the newline before the caret
|
||||
|
@ -283,7 +284,7 @@ export const kill_whole_line = wrap_input(
|
|||
* Behaves like readline's [kill_word](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC15). Deletes every character from the caret to the end of a word, with words being defined by the wordpattern setting.
|
||||
**/
|
||||
export const kill_word = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
const boundaries = getWordBoundaries(text, selectionStart, false)
|
||||
if (selectionStart < boundaries[1]) {
|
||||
boundaries[0] = selectionStart
|
||||
|
@ -304,7 +305,7 @@ export const kill_word = wrap_input(
|
|||
* Behaves like readline's [backward_kill_word](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC15). Deletes every character from the caret to the beginning of a word with word being defined by the wordpattern setting.
|
||||
**/
|
||||
export const backward_kill_word = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
const boundaries = getWordBoundaries(text, selectionStart, true)
|
||||
if (selectionStart > boundaries[0]) {
|
||||
boundaries[1] = selectionStart
|
||||
|
@ -325,7 +326,7 @@ export const backward_kill_word = wrap_input(
|
|||
* Behaves like readline's [beginning_of_line](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC12). Moves the caret to the right of the first newline character found at the left of the caret. If no newline can be found, move the caret to the beginning of the text.
|
||||
**/
|
||||
export const beginning_of_line = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
while (
|
||||
text[selectionStart - 1] !== undefined &&
|
||||
text[selectionStart - 1] !== "\n"
|
||||
|
@ -339,7 +340,7 @@ export const beginning_of_line = wrap_input(
|
|||
* Behaves like readline's [end_of_line](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC12). Moves the caret to the left of the first newline character found at the right of the caret. If no newline can be found, move the caret to the end of the text.
|
||||
**/
|
||||
export const end_of_line = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
while (
|
||||
text[selectionStart] !== undefined &&
|
||||
text[selectionStart] !== "\n"
|
||||
|
@ -352,7 +353,7 @@ export const end_of_line = wrap_input(
|
|||
/**
|
||||
* Behaves like readline's [forward_char](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC12). Moves the caret one character to the right.
|
||||
**/
|
||||
export const forward_char = wrap_input((text, selectionStart, selectionEnd) => [
|
||||
export const forward_char = wrap_input((text, selectionStart) => [
|
||||
null,
|
||||
selectionStart + 1,
|
||||
null,
|
||||
|
@ -362,14 +363,14 @@ export const forward_char = wrap_input((text, selectionStart, selectionEnd) => [
|
|||
* Behaves like readline's [backward_char](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC12). Moves the caret one character to the left.
|
||||
**/
|
||||
export const backward_char = wrap_input(
|
||||
(text, selectionStart, selectionEnd) => [null, selectionStart - 1, null],
|
||||
(text, selectionStart) => [null, selectionStart - 1, null],
|
||||
)
|
||||
|
||||
/**
|
||||
* Behaves like readline's [forward_word](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC12). Moves the caret one word to the right, with words being defined by the wordpattern setting.
|
||||
**/
|
||||
export const forward_word = wrap_input(
|
||||
needs_text((text, selectionStart, selectionEnd) => {
|
||||
needs_text((text, selectionStart) => {
|
||||
if (selectionStart === text.length) return [null, null, null]
|
||||
const boundaries = getWordBoundaries(text, selectionStart, false)
|
||||
return [null, boundaries[1], null]
|
||||
|
@ -380,7 +381,7 @@ export const forward_word = wrap_input(
|
|||
* Behaves like readline's [backward_word](http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC12). Moves the caret one word to the left, with words being defined by the wordpattern setting.
|
||||
**/
|
||||
export const backward_word = wrap_input(
|
||||
(text, selectionStart, selectionEnd) => {
|
||||
(text, selectionStart) => {
|
||||
if (selectionStart === 0) return [null, null, null]
|
||||
const boundaries = getWordBoundaries(text, selectionStart, true)
|
||||
return [null, boundaries[0], null]
|
||||
|
@ -403,3 +404,9 @@ export const rot13 = wrap_input((text, selectionStart, selectionEnd) => [
|
|||
selectionStart,
|
||||
null,
|
||||
])
|
||||
|
||||
export const jumble = wrap_input((text, selectionStart, selectionEnd) => [
|
||||
jumble_helper(text.slice(0, selectionStart) + text.slice(selectionEnd)),
|
||||
selectionStart,
|
||||
null,
|
||||
])
|
||||
|
|
|
@ -139,6 +139,7 @@ export function wrap_input(
|
|||
/**
|
||||
* Take an editor function as parameter and wrap it in a function that will handle error conditions
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
|
||||
export function needs_text(fn: editor_function, arg?: any): editor_function {
|
||||
return (
|
||||
text: string,
|
||||
|
@ -167,7 +168,6 @@ export function needs_text(fn: editor_function, arg?: any): editor_function {
|
|||
export function getLineAndColNumber(
|
||||
text: string,
|
||||
start: number,
|
||||
end: number,
|
||||
): [string, number, number] {
|
||||
const lines = text.split("\n")
|
||||
let totalChars = 0
|
||||
|
@ -194,7 +194,7 @@ export function getWordBoundaries(
|
|||
`getWordBoundaries: position (${position}) should be within text ("${text}") boundaries (0, ${text.length})`,
|
||||
)
|
||||
const pattern = new RegExp(config.get("wordpattern"), "g")
|
||||
let boundary1 = position < text.length ? position : text.length - 1
|
||||
let boundary1 = position < text.length ? position : text.length
|
||||
const direction = before ? -1 : 1
|
||||
// if the caret is not in a word, try to find the word before or after it
|
||||
// For `before`, we should check the char before the caret
|
||||
|
@ -292,3 +292,40 @@ export const charesar = (c: string, n = 13): string => {
|
|||
return String.fromCharCode(((cn - 97 + n) % 26) + 97)
|
||||
return c
|
||||
}
|
||||
|
||||
/** @hidden
|
||||
* Shuffles only letters except for the first and last letter in a word, where "word"
|
||||
* is a sequence of one of: only lowercase letters OR 5 or more uppercase letters OR an uppercase letter followed
|
||||
* by only lowercase letters.
|
||||
*/
|
||||
export const jumble_helper = (text: string): string => {
|
||||
const wordSplitRegex = new RegExp("([^a-zA-Z]|[A-Z][a-z]+)")
|
||||
return text.split(wordSplitRegex).map(jumbleWord).join("")
|
||||
}
|
||||
|
||||
function jumbleWord(word: string): string {
|
||||
if (word.length < 4 || isAcronym()) {
|
||||
return word
|
||||
}
|
||||
const innerText = word.slice(1, -1)
|
||||
return word.charAt(0) + shuffle(innerText) + word.charAt(word.length - 1)
|
||||
|
||||
function isAcronym() {
|
||||
return word.length < 5 && word.toUpperCase() === word
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles input string
|
||||
* @param text string to be shuffled
|
||||
*/
|
||||
export const shuffle = (text: string): string => {
|
||||
const arr = text.split("")
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
const j = Math.floor(Math.random() * i + 1)
|
||||
const t = arr[i]
|
||||
arr[i] = arr[j]
|
||||
arr[j] = t
|
||||
}
|
||||
return arr.join("")
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@ export const KNOWN_EXTENSIONS: { [name: string]: string } = {
|
|||
/** List of currently installed extensions.
|
||||
*/
|
||||
const installedExtensions: {
|
||||
[id: string]: browser.management.IExtensionInfo
|
||||
[id: string]: browser.management.ExtensionInfo
|
||||
} = {}
|
||||
|
||||
function updateExtensionInfo(
|
||||
extension: browser.management.IExtensionInfo,
|
||||
extension: browser.management.ExtensionInfo,
|
||||
): void {
|
||||
installedExtensions[extension.id] = extension
|
||||
}
|
||||
|
|
166
src/lib/hint_util.test.ts
Normal file
166
src/lib/hint_util.test.ts
Normal file
|
@ -0,0 +1,166 @@
|
|||
/** Testing the new argument parser for :hint */
|
||||
|
||||
import { OpenMode, HintConfig } from "@src/lib/hint_util"
|
||||
|
||||
expect.extend({
|
||||
toMatchOptions(received: HintConfig, expected: any) {
|
||||
const errors = []
|
||||
|
||||
// For each key in the expected object, check the parsed option matches the value
|
||||
for (const [key, value] of Object.entries(expected)) {
|
||||
let result: boolean
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Arrays need special handling instead of ===
|
||||
result =
|
||||
value.length == received[key].length &&
|
||||
value.every((val, i) => received[key][i] === val)
|
||||
} else {
|
||||
result = received[key] === value
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
// Add to error list when the comparison fails
|
||||
errors.push(
|
||||
`expected ${key} === ${JSON.stringify(
|
||||
value,
|
||||
)}, got ${JSON.stringify(received[key])}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { message: () => errors.join("\n"), pass: false }
|
||||
} else {
|
||||
return {
|
||||
message: () =>
|
||||
`expected ${JSON.stringify(
|
||||
received,
|
||||
)} to not match ${JSON.stringify(expected)}`,
|
||||
pass: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Add this type definition so Jest/TypeScript doesn't complain
|
||||
declare global {
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
toMatchOptions(expected: any): CustomMatcherResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Most test cases are from GitHub advanced search:
|
||||
// https://github.com/search?q=hint+filename%3Atridactylrc+filename%3A.tridactylrc&type=Code&ref=advsearch&l=&l=
|
||||
//
|
||||
// Expected results are written based on what the doc (spec) specifies, and the
|
||||
// new parser should observe those options correctly.
|
||||
const testCases = [
|
||||
{
|
||||
sources: ["-b"],
|
||||
expected: {
|
||||
openMode: OpenMode.BackgroundTab,
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: ["-br", "-qb"],
|
||||
expected: {
|
||||
rapid: true,
|
||||
openMode: OpenMode.BackgroundTab,
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: ["-i"],
|
||||
expected: {
|
||||
openMode: OpenMode.Images,
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: ['-c [class*="expand"],[class="togg"]'],
|
||||
expected: {
|
||||
selectors: ['[class*="expand"],[class="togg"]'],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: [
|
||||
"-cF img i => tri.excmds.yankimage(tri.urlutils.getAbsoluteURL(i.src))",
|
||||
],
|
||||
expected: {
|
||||
selectors: ["img"],
|
||||
callback:
|
||||
"i => tri.excmds.yankimage(tri.urlutils.getAbsoluteURL(i.src))",
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: [
|
||||
"-qW js -p tri.excmds.shellescape(JS_ARG).then(url => tri.excmds.exclaim_quiet('~/scripts/mpv/append ' + url))",
|
||||
],
|
||||
expected: {
|
||||
rapid: true,
|
||||
excmd:
|
||||
"js -p tri.excmds.shellescape(JS_ARG).then(url => tri.excmds.exclaim_quiet('~/scripts/mpv/append ' + url))",
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: ["-Jbc [data-test-id=post-content] a,.Comment a"],
|
||||
expected: {
|
||||
jshints: false,
|
||||
openMode: OpenMode.BackgroundTab,
|
||||
selectors: ["[data-test-id=post-content]", "a,.Comment", "a"],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: ["-Jc .rc > .r > a"],
|
||||
expected: {
|
||||
jshints: false,
|
||||
selectors: [".rc", ">", ".r", ">", "a"],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: [
|
||||
"-Jcb ul:not(#duckbar_dropdowns) .zcm__item .zcm__link,.result__a,.result--more",
|
||||
],
|
||||
expected: {
|
||||
jshints: false,
|
||||
openMode: OpenMode.BackgroundTab,
|
||||
selectors: [
|
||||
"ul:not(#duckbar_dropdowns)",
|
||||
".zcm__item",
|
||||
".zcm__link,.result__a,.result--more",
|
||||
],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sources: ["-qpipe a href"],
|
||||
expected: {
|
||||
rapid: true,
|
||||
pipeAttribute: "href",
|
||||
selectors: ["a"],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Check all test cases
|
||||
for (const { sources, expected } of testCases) {
|
||||
for (const source of sources) {
|
||||
// Split the command line into arguments
|
||||
const args = source.split(" ")
|
||||
|
||||
// Parse and compare
|
||||
test(source, () =>
|
||||
expect(HintConfig.parse(args)).toMatchOptions(expected),
|
||||
)
|
||||
}
|
||||
}
|
424
src/lib/hint_util.ts
Normal file
424
src/lib/hint_util.ts
Normal file
|
@ -0,0 +1,424 @@
|
|||
/**
|
||||
* Helper types for :hint
|
||||
*/
|
||||
|
||||
import Logger from "@src/lib/logging"
|
||||
import * as DOM from "@src/lib/dom"
|
||||
import * as hinting from "@src/content/hinting"
|
||||
|
||||
/**
|
||||
* Open mode: how to act on the selected hintable element
|
||||
*/
|
||||
export enum OpenMode {
|
||||
Default = "",
|
||||
Tab = "-t",
|
||||
BackgroundTab = "-b",
|
||||
Window = "-w",
|
||||
WindowPrivate = "-wp",
|
||||
Highlight = "-h",
|
||||
Images = "-i",
|
||||
ImagesTab = "-I",
|
||||
Kill = "-k",
|
||||
KillTridactyl = "-K",
|
||||
Scroll = "-z",
|
||||
SaveResource = "-s",
|
||||
SaveImage = "-S",
|
||||
SaveAsResource = "-a",
|
||||
SaveAsImage = "-A",
|
||||
ScrollFocus = "-;",
|
||||
TTSRead = "-r",
|
||||
YankAlt = "-P",
|
||||
YankAnchor = "-#",
|
||||
YankLink = "-y",
|
||||
YankText = "-p",
|
||||
}
|
||||
|
||||
/**
|
||||
* Hinting parameters interface
|
||||
*/
|
||||
export interface HintOptions {
|
||||
rapid: boolean
|
||||
textFilter: null | string | RegExp
|
||||
openMode: OpenMode
|
||||
includeInvisible: boolean
|
||||
immediate: boolean
|
||||
jshints: boolean
|
||||
callback: null | string
|
||||
excmd: null | string
|
||||
pipeAttribute: null | string
|
||||
selectors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Hinting parameters class for parsing
|
||||
*/
|
||||
export class HintConfig implements HintOptions {
|
||||
public rapid = false
|
||||
public textFilter = null
|
||||
public openMode = OpenMode.Default
|
||||
public includeInvisible = false
|
||||
public immediate = false
|
||||
public jshints = true
|
||||
public callback = null
|
||||
public excmd = null
|
||||
public pipeAttribute = null
|
||||
public selectors = []
|
||||
public warnings = []
|
||||
|
||||
public static parse(args: string[]): HintConfig {
|
||||
// Argument parser state
|
||||
enum State {
|
||||
Initial,
|
||||
ExpectF,
|
||||
ExpectFR,
|
||||
ExpectCallback,
|
||||
ExpectExcmd,
|
||||
ExpectSelector,
|
||||
ExpectPipeSelector,
|
||||
ExpectPipeAttribute,
|
||||
ExpectSelectorCallback,
|
||||
}
|
||||
|
||||
const result = new HintConfig()
|
||||
const multiLetterFlags = ["fr", "wp", "br", "pipe"]
|
||||
|
||||
// Parser state
|
||||
let state = State.Initial
|
||||
|
||||
outer: for (let argI = 0; argI < args.length; ++argI) {
|
||||
const arg = args[argI]
|
||||
|
||||
switch (state) {
|
||||
case State.Initial:
|
||||
if (arg.length >= 2 && arg[0] === "-" && arg[1] !== "-") {
|
||||
// Parse short arguments, i.e. - followed by (mostly) single-letter arguments,
|
||||
// and some two-letter arguments.
|
||||
|
||||
for (let i = 1; i < arg.length; ++i) {
|
||||
let flag = arg[i]
|
||||
|
||||
// Fix two-letter flags using lookahead
|
||||
if (i < arg.length - 1) {
|
||||
const multiLetterFlag = multiLetterFlags.find(
|
||||
tlf =>
|
||||
arg.substring(i, i + tlf.length) ===
|
||||
tlf,
|
||||
)
|
||||
|
||||
if (multiLetterFlag !== undefined) {
|
||||
flag = multiLetterFlag
|
||||
i += multiLetterFlag.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Process flag
|
||||
let newOpenMode: undefined | OpenMode
|
||||
let newState: undefined | State
|
||||
|
||||
// eslint-disable-next-line sonarjs/max-switch-cases
|
||||
switch (flag) {
|
||||
case "br":
|
||||
// Equivalent to -qb, but deprecated
|
||||
result.rapid = true
|
||||
newOpenMode = OpenMode.BackgroundTab
|
||||
break
|
||||
case "q":
|
||||
result.rapid = true
|
||||
break
|
||||
case "f":
|
||||
newState = State.ExpectF
|
||||
break
|
||||
case "fr":
|
||||
newState = State.ExpectFR
|
||||
break
|
||||
case "V":
|
||||
result.includeInvisible = true
|
||||
break
|
||||
case "J":
|
||||
result.jshints = false
|
||||
break
|
||||
case "!":
|
||||
result.immediate = true
|
||||
break
|
||||
case "F":
|
||||
newState = State.ExpectCallback
|
||||
break
|
||||
case "W":
|
||||
newState = State.ExpectExcmd
|
||||
break
|
||||
case "c":
|
||||
newState = State.ExpectSelector
|
||||
break
|
||||
case "pipe":
|
||||
newState = State.ExpectPipeSelector
|
||||
break
|
||||
case "t":
|
||||
newOpenMode = OpenMode.Tab
|
||||
break
|
||||
case "b":
|
||||
newOpenMode = OpenMode.BackgroundTab
|
||||
break
|
||||
case "w":
|
||||
newOpenMode = OpenMode.Window
|
||||
break
|
||||
case "wp":
|
||||
newOpenMode = OpenMode.WindowPrivate
|
||||
break
|
||||
case "h":
|
||||
newOpenMode = OpenMode.Highlight
|
||||
break
|
||||
case "i":
|
||||
newOpenMode = OpenMode.Images
|
||||
break
|
||||
case "I":
|
||||
newOpenMode = OpenMode.ImagesTab
|
||||
break
|
||||
case "k":
|
||||
newOpenMode = OpenMode.Kill
|
||||
break
|
||||
case "K":
|
||||
newOpenMode = OpenMode.KillTridactyl
|
||||
break
|
||||
case "z":
|
||||
newOpenMode = OpenMode.Scroll
|
||||
break
|
||||
case "s":
|
||||
newOpenMode = OpenMode.SaveResource
|
||||
break
|
||||
case "S":
|
||||
newOpenMode = OpenMode.SaveImage
|
||||
break
|
||||
case "a":
|
||||
newOpenMode = OpenMode.SaveAsResource
|
||||
break
|
||||
case "A":
|
||||
newOpenMode = OpenMode.SaveAsImage
|
||||
break
|
||||
case ";":
|
||||
newOpenMode = OpenMode.ScrollFocus
|
||||
break
|
||||
case "r":
|
||||
newOpenMode = OpenMode.TTSRead
|
||||
break
|
||||
case "P":
|
||||
newOpenMode = OpenMode.YankAlt
|
||||
break
|
||||
case "#":
|
||||
newOpenMode = OpenMode.YankAnchor
|
||||
break
|
||||
case "y":
|
||||
newOpenMode = OpenMode.YankLink
|
||||
break
|
||||
case "p":
|
||||
newOpenMode = OpenMode.YankText
|
||||
break
|
||||
default:
|
||||
result.warnings.push(
|
||||
`unknown flag -${flag}`,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// Check openMode changes
|
||||
if (newOpenMode !== undefined) {
|
||||
if (result.openMode !== OpenMode.Default) {
|
||||
// Notify that multiple open modes doesn't make sense
|
||||
result.warnings.push(
|
||||
"multiple open mode flags specified, overriding the previous ones",
|
||||
)
|
||||
}
|
||||
|
||||
result.openMode = newOpenMode
|
||||
}
|
||||
|
||||
// Check state changes
|
||||
if (newState !== undefined) {
|
||||
// Some state transitions are dubious, specifically all the ones that go from (argument
|
||||
// expecting value) to (other argument expecting value), except for -cF
|
||||
if (
|
||||
(state === State.ExpectSelector &&
|
||||
newState === State.ExpectCallback) ||
|
||||
(state === State.ExpectCallback &&
|
||||
newState === State.ExpectSelector)
|
||||
) {
|
||||
newState = State.ExpectSelectorCallback
|
||||
}
|
||||
|
||||
if (
|
||||
state !== State.Initial &&
|
||||
newState !== State.ExpectSelectorCallback
|
||||
) {
|
||||
result.warnings.push(
|
||||
`multiple flags taking a value were specified, only the last one (-${flag}) will be processed`,
|
||||
)
|
||||
}
|
||||
|
||||
state = newState
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not something that looks like an argument, add it to positionals for later processing
|
||||
result.selectors.push(arg)
|
||||
}
|
||||
break
|
||||
case State.ExpectF:
|
||||
case State.ExpectFR:
|
||||
// Collect arguments using escapes
|
||||
let filter = arg
|
||||
while (filter.endsWith("\\")) {
|
||||
filter = filter.substring(0, filter.length - 1)
|
||||
|
||||
if (argI + 1 < args.length) {
|
||||
filter += " " + args[++argI]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (state == State.ExpectF) {
|
||||
// -f
|
||||
result.textFilter = filter
|
||||
} else {
|
||||
// -fr
|
||||
result.textFilter = new RegExp(filter)
|
||||
}
|
||||
|
||||
state = State.Initial
|
||||
|
||||
break
|
||||
case State.ExpectExcmd:
|
||||
// Collect all the remaining arguments into a excmd callback
|
||||
result.excmd = args.slice(argI).join(" ")
|
||||
// Reset state to initial, parsing was successful
|
||||
state = State.Initial
|
||||
break outer
|
||||
case State.ExpectCallback:
|
||||
// Collect all the remaining arguments into a Javascript callback
|
||||
result.callback = args.slice(argI).join(" ")
|
||||
// Reset state to initial, parsing was successful
|
||||
state = State.Initial
|
||||
break outer
|
||||
case State.ExpectSelector:
|
||||
// -c, expect a single selector
|
||||
result.selectors.push(arg)
|
||||
state = State.Initial
|
||||
break
|
||||
case State.ExpectPipeSelector:
|
||||
// -pipe, first expect a selector
|
||||
result.selectors.push(arg)
|
||||
// Then, expect the attribute
|
||||
state = State.ExpectPipeAttribute
|
||||
break
|
||||
case State.ExpectPipeAttribute:
|
||||
// -pipe, second argument
|
||||
result.pipeAttribute = arg
|
||||
// Keep parsing options when we're done
|
||||
state = State.Initial
|
||||
break
|
||||
case State.ExpectSelectorCallback:
|
||||
// -cF, expect selector, then callback
|
||||
result.selectors.push(arg)
|
||||
state = State.ExpectCallback
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== State.Initial) {
|
||||
// If we didn't return to the initial state, we were expecting an option value
|
||||
result.warnings.push("error parsing options: expected a value")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public printWarnings(logger: Logger) {
|
||||
for (const warning of this.warnings) {
|
||||
logger.warning(warning)
|
||||
}
|
||||
}
|
||||
|
||||
defaultHintables() {
|
||||
// Use the default selectors to find hintable elements
|
||||
switch (this.openMode) {
|
||||
case OpenMode.YankText:
|
||||
case OpenMode.Highlight:
|
||||
case OpenMode.Scroll:
|
||||
// For text-based opens, look for elements with text by default
|
||||
return hinting.toHintablesArray(
|
||||
DOM.elementsWithText(this.includeInvisible),
|
||||
)
|
||||
|
||||
case OpenMode.YankAlt:
|
||||
return hinting.toHintablesArray(
|
||||
DOM.getElemsBySelector("[title],[alt]", [
|
||||
DOM.isVisibleFilter(this.includeInvisible),
|
||||
]),
|
||||
)
|
||||
|
||||
case OpenMode.YankAnchor:
|
||||
return hinting.toHintablesArray(
|
||||
DOM.anchors(this.includeInvisible),
|
||||
)
|
||||
|
||||
case OpenMode.Images:
|
||||
case OpenMode.ImagesTab:
|
||||
case OpenMode.SaveImage:
|
||||
case OpenMode.SaveAsImage:
|
||||
return hinting.toHintablesArray(
|
||||
hinting.hintableImages(this.includeInvisible),
|
||||
)
|
||||
|
||||
case OpenMode.Kill:
|
||||
case OpenMode.KillTridactyl:
|
||||
return hinting.toHintablesArray(
|
||||
hinting.killables(this.includeInvisible),
|
||||
)
|
||||
|
||||
case OpenMode.SaveResource:
|
||||
case OpenMode.SaveAsResource:
|
||||
return hinting.toHintablesArray(
|
||||
hinting.saveableElements(this.includeInvisible),
|
||||
)
|
||||
|
||||
default:
|
||||
return hinting.hintables(
|
||||
DOM.HINTTAGS_selectors,
|
||||
this.jshints,
|
||||
this.includeInvisible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public hintables() {
|
||||
// User selectors always override default built-ins
|
||||
const hintables =
|
||||
this.selectors.length > 0
|
||||
? hinting.hintables(
|
||||
this.selectors.join(" "),
|
||||
this.jshints,
|
||||
this.includeInvisible,
|
||||
)
|
||||
: this.defaultHintables()
|
||||
|
||||
// Do we have text filters to refine this?
|
||||
if (this.textFilter !== null) {
|
||||
for (const elements of hintables) {
|
||||
elements.elements = elements.elements.filter(
|
||||
hinting.hintByTextFilter(this.textFilter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return hintables
|
||||
}
|
||||
|
||||
public get isYank() {
|
||||
return (
|
||||
this.openMode === OpenMode.YankAnchor ||
|
||||
this.openMode === OpenMode.YankAlt ||
|
||||
this.openMode === OpenMode.YankLink ||
|
||||
this.openMode === OpenMode.YankText
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
try {
|
||||
// spread operator and template strings support
|
||||
(function testSpreadOpAndTemplate() {
|
||||
const tag = function tag(strings, ...values) {
|
||||
const tag = function tag() {
|
||||
return
|
||||
}
|
||||
// We don't need this value - we're just checking if its attempted creation causes any errors
|
||||
|
|
|
@ -174,7 +174,7 @@ export function parse(keyseq: KeyEventLike[], map: KeyMap): ParserResponse {
|
|||
try {
|
||||
const perfect = find(
|
||||
possibleMappings,
|
||||
([k, v]) => k.length === keyseq.length,
|
||||
([k, _v]) => k.length === keyseq.length,
|
||||
)
|
||||
return {
|
||||
value: perfect[1],
|
||||
|
@ -212,7 +212,7 @@ function prefixes(seq1: KeyEventLike[], seq2: MinimalKey[]) {
|
|||
/** returns the fragment of `map` that keyseq is a valid prefix of. */
|
||||
export function completions(keyseq: KeyEventLike[], map: KeyMap): KeyMap {
|
||||
return new Map(
|
||||
filter(map.entries(), ([ks, maptarget]) => prefixes(keyseq, ks)),
|
||||
filter(map.entries(), ([ks, _maptarget]) => prefixes(keyseq, ks)),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -357,6 +357,7 @@ export const commandKey2jsKey = {
|
|||
Down: "ArrowDown",
|
||||
Left: "ArrowLeft",
|
||||
Right: "ArrowRight",
|
||||
Space: " ",
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -388,7 +389,9 @@ export function minimalKeyToMozMap(key: MinimalKey): string {
|
|||
key.ctrlKey && mozMap.push("MacCtrl")
|
||||
key.shiftKey && mozMap.push("Shift")
|
||||
key.metaKey && mozMap.push("Command")
|
||||
const jsKey2commandKey = R.invertObj(commandKey2jsKey)
|
||||
const jsKey2commandKey = Object.fromEntries(
|
||||
Object.entries(commandKey2jsKey).map(([key, value]) => [value, key]),
|
||||
)
|
||||
mozMap.push(R.propOr(key.key.toUpperCase(), key.key, jsKey2commandKey))
|
||||
return mozMap.join("+")
|
||||
}
|
||||
|
@ -418,6 +421,9 @@ export function translateKeysInPlace(keys, conf): void {
|
|||
export function keyMap(conf): KeyMap {
|
||||
if (KEYMAP_CACHE[conf]) return KEYMAP_CACHE[conf]
|
||||
|
||||
// Fail silently and pass keys through to page if Tridactyl hasn't loaded yet
|
||||
if (!config.INITIALISED) return new Map()
|
||||
|
||||
const mapobj: { [keyseq: string]: string } = config.get(conf)
|
||||
if (mapobj === undefined)
|
||||
throw new Error(
|
||||
|
@ -501,7 +507,7 @@ export function translateKeysUsingKeyTranslateMap(
|
|||
|
||||
// }}}
|
||||
|
||||
browser.storage.onChanged.addListener((changes, areaname) => {
|
||||
browser.storage.onChanged.addListener(changes => {
|
||||
if ("userconfig" in changes) {
|
||||
KEYMAP_CACHE = {}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export class Logger {
|
|||
|
||||
// do nothing with the message
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return function (...args) {}
|
||||
return function () {}
|
||||
}
|
||||
|
||||
// These are all getters so that logger.debug = console.debug and
|
||||
|
|
|
@ -88,14 +88,13 @@ function backgroundHandler<
|
|||
>(
|
||||
root: Root,
|
||||
message: TypedMessage<Root, Type, Command>,
|
||||
sender: browser.runtime.MessageSender,
|
||||
): ReturnType<Root[Type][Command]> {
|
||||
return root[message.type][message.command](...message.args)
|
||||
}
|
||||
|
||||
export function setupListener<Root>(root: Root) {
|
||||
browser.runtime.onMessage.addListener(
|
||||
(message: any, sender: browser.runtime.MessageSender) => {
|
||||
(message: any) => {
|
||||
if (message.type in root) {
|
||||
if (!(message.command in root[message.type]))
|
||||
throw new Error(
|
||||
|
@ -105,19 +104,19 @@ export function setupListener<Root>(root: Root) {
|
|||
throw new Error(
|
||||
`wrong arguments in protocol ${message.type} ${message.command}`,
|
||||
)
|
||||
return Promise.resolve(backgroundHandler(root, message, sender))
|
||||
return Promise.resolve(backgroundHandler(root, message))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type StripPromise<T> = T extends Promise<infer U> ? U : T
|
||||
// type StripPromise<T> = T extends Promise<infer U> ? U : T
|
||||
|
||||
/** Send a message to non-content scripts */
|
||||
export async function message<
|
||||
Type extends keyof Messages.Background,
|
||||
Command extends keyof Messages.Background[Type],
|
||||
F extends ((...args: any) => any) & Messages.Background[Type][Command]
|
||||
F extends ((...args: any[]) => any) & Messages.Background[Type][Command]
|
||||
>(type: Type, command: Command, ...args: Parameters<F>) {
|
||||
const message: TypedMessage<Messages.Background, Type, Command> = {
|
||||
type,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Background functions for the native messenger interface
|
||||
*/
|
||||
|
||||
import * as semverCompare from "semver-compare"
|
||||
import semverCompare from "semver-compare"
|
||||
import * as config from "@src/lib/config"
|
||||
import { browserBg, getContext } from "@src/lib/webext"
|
||||
|
||||
|
@ -13,6 +13,7 @@ const NATIVE_NAME = "tridactyl"
|
|||
type MessageCommand =
|
||||
| "version"
|
||||
| "run"
|
||||
| "run_async"
|
||||
| "read"
|
||||
| "write"
|
||||
| "writerc"
|
||||
|
@ -20,11 +21,12 @@ type MessageCommand =
|
|||
| "list_dir"
|
||||
| "mkdir"
|
||||
| "move"
|
||||
| "eval"
|
||||
| "eval" // Only works in native < 0.2.0 (NB: use "run" for non-Python eval)
|
||||
| "getconfig"
|
||||
| "getconfigpath"
|
||||
| "env"
|
||||
| "win_firefox_restart"
|
||||
| "ppid" // Removed from Windows since native >= 0.2.0
|
||||
interface MessageResp {
|
||||
cmd: string
|
||||
version: string | null
|
||||
|
@ -58,12 +60,21 @@ async function sendNativeMsg(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getrcpath(): Promise<string> {
|
||||
export async function getrcpath(
|
||||
separator: "unix" | "auto" = "auto",
|
||||
): Promise<string> {
|
||||
const res = await sendNativeMsg("getconfigpath", {})
|
||||
|
||||
if (res.code !== 0) throw new Error("getrcpath error: " + res.code)
|
||||
|
||||
return res.content
|
||||
if (
|
||||
separator == "unix" &&
|
||||
(await browserBg.runtime.getPlatformInfo()).os == "win"
|
||||
) {
|
||||
return res.content.replace(/\\/g, "/")
|
||||
} else {
|
||||
return res.content
|
||||
}
|
||||
}
|
||||
|
||||
export async function getrc(): Promise<string> {
|
||||
|
@ -78,9 +89,13 @@ export async function getrc(): Promise<string> {
|
|||
}
|
||||
}
|
||||
|
||||
let NATIVE_VERSION_CACHE: string
|
||||
export async function getNativeMessengerVersion(
|
||||
quiet = false,
|
||||
): Promise<string> {
|
||||
if (NATIVE_VERSION_CACHE !== undefined) {
|
||||
return NATIVE_VERSION_CACHE
|
||||
}
|
||||
const res = await sendNativeMsg("version", {}, quiet)
|
||||
if (res === undefined) {
|
||||
if (quiet) return undefined
|
||||
|
@ -88,96 +103,128 @@ export async function getNativeMessengerVersion(
|
|||
}
|
||||
if (res.version && !res.error) {
|
||||
logger.info(`Native version: ${res.version}`)
|
||||
return res.version.toString()
|
||||
NATIVE_VERSION_CACHE = res.version.toString()
|
||||
// Wipe cache after 500ms
|
||||
setTimeout(() => (NATIVE_VERSION_CACHE = undefined), 500)
|
||||
return NATIVE_VERSION_CACHE
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBestEditor(): Promise<string> {
|
||||
let gui_candidates = []
|
||||
let term_emulators = []
|
||||
let tui_editors = []
|
||||
let last_resorts = []
|
||||
const gui_candidates: string[] = []
|
||||
const term_emulators: string[] = []
|
||||
const tui_editors: string[] = []
|
||||
const last_resorts: string[] = []
|
||||
const os = (await browserBg.runtime.getPlatformInfo()).os
|
||||
const arg_quote = os === "win" ? '"' : "'"
|
||||
const vim_positioning_arg = ` ${arg_quote}+normal!%lGzv%c|${arg_quote}`
|
||||
if (os === "mac") {
|
||||
gui_candidates = [
|
||||
"/Applications/MacVim.app/Contents/bin/mvim -f" +
|
||||
vim_positioning_arg,
|
||||
"/usr/local/bin/vimr --wait --nvim +only",
|
||||
]
|
||||
gui_candidates.push(
|
||||
...[
|
||||
"/Applications/MacVim.app/Contents/bin/mvim -f" +
|
||||
vim_positioning_arg,
|
||||
"/usr/local/bin/vimr --wait --nvim +only",
|
||||
],
|
||||
)
|
||||
// if anyone knows of any "sensible" terminals that let you send them commands to run,
|
||||
// please let us know in issue #451!
|
||||
term_emulators = [
|
||||
"/Applications/cool-retro-term.app/Contents/MacOS/cool-retro-term -e",
|
||||
]
|
||||
last_resorts = ["open -nWt"]
|
||||
term_emulators.push(
|
||||
...[
|
||||
"/Applications/cool-retro-term.app/Contents/MacOS/cool-retro-term -e",
|
||||
],
|
||||
)
|
||||
last_resorts.push(...["open -nWt"])
|
||||
} else {
|
||||
// Tempted to put this behind another config setting: prefergui
|
||||
gui_candidates = ["gvim -f" + vim_positioning_arg]
|
||||
gui_candidates.push(...["gvim -f" + vim_positioning_arg])
|
||||
|
||||
// we generally try to give the terminal the class "tridactyl_editor" so that
|
||||
// it can be made floating, e.g in i3:
|
||||
// for_window [class="tridactyl_editor"] floating enable border pixel 1
|
||||
term_emulators = [
|
||||
"st -c tridactyl_editor",
|
||||
"xterm -class tridactyl_editor -e",
|
||||
"uxterm -class tridactyl_editor -e",
|
||||
"urxvt -e",
|
||||
"alacritty -e", // alacritty is nice but takes ages to start and doesn't support class
|
||||
// Terminator and termite require -e commands to be in quotes
|
||||
'terminator -u -e "%c"',
|
||||
'termite --class tridactyl_editor -e "%c"',
|
||||
"sakura --class tridactyl_editor -e",
|
||||
"lilyterm -e",
|
||||
"mlterm -e",
|
||||
"roxterm -e",
|
||||
"cool-retro-term -e",
|
||||
// Gnome-terminal doesn't work consistently, see issue #1035
|
||||
// "dbus-launch gnome-terminal --",
|
||||
// These terminal emulators can't normally be run on Windows, usually because they require X11.
|
||||
if (os === "linux" || os === "openbsd") {
|
||||
term_emulators.push(
|
||||
...[
|
||||
// we generally try to give the terminal the class "tridactyl_editor" so that
|
||||
// it can be made floating, e.g in i3:
|
||||
// for_window [class="tridactyl_editor"] floating enable border pixel 1
|
||||
"st -c tridactyl_editor",
|
||||
"xterm -class tridactyl_editor -e",
|
||||
"uxterm -class tridactyl_editor -e",
|
||||
"urxvt -e",
|
||||
'termite --class tridactyl_editor -e "%c"',
|
||||
"sakura --class tridactyl_editor -e",
|
||||
"lilyterm -e",
|
||||
"mlterm -N tridactyl_editor -e",
|
||||
"roxterm -e",
|
||||
"cool-retro-term -e",
|
||||
// Terminator doesn't appear to honour -c, but the option is
|
||||
// documented in its manpage and seems to cause no errors when supplied.
|
||||
'terminator -u -c tridactyl_editor -e "%c"',
|
||||
// Gnome-terminal doesn't work consistently, see issue #1035
|
||||
// "dbus-launch gnome-terminal --",
|
||||
],
|
||||
)
|
||||
}
|
||||
if (os === "win") {
|
||||
term_emulators.push(
|
||||
...["conemu -run", "mintty --class tridactyl_editor -e"],
|
||||
)
|
||||
if (await nativegate("0.2.1", false)) {
|
||||
term_emulators.push("start /wait")
|
||||
}
|
||||
}
|
||||
// These terminal emulators are cross-platform.
|
||||
term_emulators.push(
|
||||
...[
|
||||
"alacritty --class tridactyl_editor -e",
|
||||
|
||||
// I wanted to put hyper.js here as a joke but you can't start it running a command,
|
||||
// which is a far better joke: a terminal emulator that you can't send commands to.
|
||||
// You win this time, web artisans.
|
||||
]
|
||||
last_resorts = [
|
||||
"emacs",
|
||||
"gedit",
|
||||
"kate",
|
||||
"abiword",
|
||||
"sublime",
|
||||
"atom -w",
|
||||
]
|
||||
// I wanted to put hyper.js here as a joke but you can't start it running a command,
|
||||
// which is a far better joke: a terminal emulator that you can't send commands to.
|
||||
// You win this time, web artisans.
|
||||
// Still true in 2021.
|
||||
],
|
||||
)
|
||||
last_resorts.push(
|
||||
...[
|
||||
"emacs",
|
||||
"gedit",
|
||||
"kate",
|
||||
"sublime",
|
||||
"atom -w",
|
||||
"code -nw",
|
||||
"abiword",
|
||||
"notepad",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
tui_editors = [
|
||||
"vim" + vim_positioning_arg,
|
||||
"nvim" + vim_positioning_arg,
|
||||
"nano %f",
|
||||
"emacs -nw %f",
|
||||
]
|
||||
tui_editors.push(
|
||||
...[
|
||||
"vim" + vim_positioning_arg,
|
||||
"nvim" + vim_positioning_arg,
|
||||
"nano %f",
|
||||
"emacs -nw %f",
|
||||
],
|
||||
)
|
||||
|
||||
// Consider GUI editors
|
||||
let cmd = await firstinpath(gui_candidates)
|
||||
// Try GUI editors.
|
||||
const guicmd: string = await firstinpath(gui_candidates)
|
||||
if (guicmd) {
|
||||
return guicmd
|
||||
}
|
||||
|
||||
if (cmd === undefined) {
|
||||
// Try to find a terminal emulator
|
||||
cmd = await firstinpath(term_emulators)
|
||||
if (cmd !== undefined) {
|
||||
// and a text editor
|
||||
const tuicmd = await firstinpath(tui_editors)
|
||||
if (cmd.includes("%c")) {
|
||||
cmd = cmd.replace("%c", tuicmd)
|
||||
} else {
|
||||
cmd = cmd + " " + tuicmd
|
||||
}
|
||||
// Try TUI editors.
|
||||
const termcmd: string = await firstinpath(term_emulators)
|
||||
const tuicmd: string = await firstinpath(tui_editors)
|
||||
if (termcmd && tuicmd) {
|
||||
if (termcmd.includes("%c")) {
|
||||
return tuicmd.replace("%c", tuicmd)
|
||||
} else {
|
||||
// or fall back to some really stupid stuff
|
||||
cmd = await firstinpath(last_resorts)
|
||||
return termcmd + " " + tuicmd
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
// If all else fails, try some stupid stuff to scare users into setting
|
||||
// their editorcmd.
|
||||
return await firstinpath(last_resorts)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -307,10 +354,24 @@ export async function temp(content: string, prefix: string) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function move(from: string, to: string) {
|
||||
return sendNativeMsg("move", { from, to }).catch(e => {
|
||||
throw new Error(`Failed to move '${from}' to '${to}'. ${e}.`)
|
||||
})
|
||||
export async function move(
|
||||
from: string,
|
||||
to: string,
|
||||
overwrite: boolean,
|
||||
cleanup: boolean,
|
||||
) {
|
||||
const requiredNativeMessengerVersion = "0.3.0"
|
||||
if ((await nativegate(requiredNativeMessengerVersion, false))) {
|
||||
return sendNativeMsg("move", { from, to, overwrite, cleanup }).catch(e => {
|
||||
throw new Error(`Failed to move '${from}' to '${to}'. ${e}.`)
|
||||
})
|
||||
} else {
|
||||
// older "saveas" scenario for native-messenger < 0.3.0
|
||||
return sendNativeMsg("move", { from, to }).catch(e => {
|
||||
throw new Error(`Failed to move '${from}' to '${to}'. ${e}.`)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function listDir(dir: string) {
|
||||
|
@ -340,8 +401,20 @@ export async function run(command: string, content = "") {
|
|||
return msg
|
||||
}
|
||||
|
||||
export async function runAsync(command: string) {
|
||||
const required_version = "0.3.1"
|
||||
if (!await nativegate(required_version, false)) {
|
||||
throw new Error(
|
||||
`runAsync needs native messenger version >= ${required_version}.`,
|
||||
)
|
||||
}
|
||||
logger.info(await sendNativeMsg("run_async", { command }))
|
||||
}
|
||||
|
||||
/** Evaluates a string in the native messenger. This has to be python code. If
|
||||
* you want to run shell strings, use run() instead.
|
||||
*
|
||||
* Only works for native messenger versions < 0.2.0.
|
||||
*/
|
||||
export async function pyeval(command: string): Promise<MessageResp> {
|
||||
return sendNativeMsg("eval", { command })
|
||||
|
@ -412,21 +485,54 @@ export async function clipboard(
|
|||
throw new Error("Unknown action!")
|
||||
}
|
||||
|
||||
/** This returns the commandline that was used to start firefox.
|
||||
You'll get both firefox binary (not necessarily an absolute path) and flags */
|
||||
/**
|
||||
* This returns the commandline that was used to start Firefox.
|
||||
* You'll get both the binary (not necessarily an absolute path) and flags.
|
||||
*/
|
||||
export async function ff_cmdline(): Promise<string[]> {
|
||||
// Using ' and + rather that ` because we don't want newlines
|
||||
let output: MessageResp
|
||||
if ((await browserBg.runtime.getPlatformInfo()).os === "win") {
|
||||
throw new Error(
|
||||
`Error: "ff_cmdline() is currently broken on Windows and should be avoided."`,
|
||||
)
|
||||
if (!(await nativegate("0.3.3", false))) {
|
||||
const browser_name = await config.get("browser")
|
||||
output = await run(
|
||||
`powershell -NoProfile -Command "\
|
||||
$processes = Get-CimInstance -Property ProcessId,ParentProcessId,Name,CommandLine -ClassName Win32_Process;\
|
||||
if (-not ($processes | where { $_.Name -match '^${browser_name}' })) { exit 1; };\
|
||||
$ppid = ($processes | where { $_.ProcessId -EQ $PID }).ParentProcessId;\
|
||||
$pproc = $processes | where { $_.ProcessId -EQ $ppid };\
|
||||
while ($pproc.Name -notmatch '^${browser_name}') {\
|
||||
$ppid = $pproc.ParentProcessId;\
|
||||
$pproc = $processes | where { $_.ProcessId -EQ $ppid };\
|
||||
};\
|
||||
Write-Output $pproc.CommandLine;\
|
||||
"`,
|
||||
)
|
||||
} else {
|
||||
output = await run(
|
||||
`powershell -NoProfile -Command "\
|
||||
Get-CimInstance -Property CommandLine,ProcessId -ClassName Win32_Process \
|
||||
| where { $_.ProcessId -EQ ${(await sendNativeMsg("ppid", {})).content} } \
|
||||
| select -ExpandProperty CommandLine | Write-Output\
|
||||
"`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const output = await pyeval(
|
||||
'handleMessage({"cmd": "run", ' +
|
||||
'"command": "ps -p " + str(os.getppid()) + " -oargs="})["content"]',
|
||||
)
|
||||
return output.content.trim().split(" ")
|
||||
const actualVersion = await getNativeMessengerVersion()
|
||||
|
||||
// Backwards-compat for Python native messenger
|
||||
if (semverCompare("0.2.0", actualVersion) > 0) {
|
||||
output = await pyeval(
|
||||
// Using ' and + rather than ` because we don't want newlines
|
||||
'handleMessage({"cmd": "run", ' +
|
||||
'"command": "ps -p " + str(os.getppid()) + " -oargs="})["content"]',
|
||||
)
|
||||
} else {
|
||||
const ppid = (await sendNativeMsg("ppid", {})).content.trim()
|
||||
output = await run("ps -p " + ppid + " -oargs=")
|
||||
}
|
||||
output.content = output.content.replace("\n", "")
|
||||
}
|
||||
return output.content.trim().split(" ")
|
||||
}
|
||||
|
||||
export async function parseProfilesIni(content: string, basePath: string) {
|
||||
|
@ -521,7 +627,7 @@ export async function getProfileUncached() {
|
|||
}
|
||||
|
||||
// Then, try to find a profile path in the arguments given to Firefox
|
||||
const cmdline = await ff_cmdline().catch(e => "")
|
||||
const cmdline = await ff_cmdline().catch(() => "")
|
||||
let profile = cmdline.indexOf("--profile")
|
||||
if (profile === -1) profile = cmdline.indexOf("-profile")
|
||||
if (profile >= 0 && profile < cmdline.length - 1) {
|
||||
|
@ -619,7 +725,7 @@ export async function getProfile() {
|
|||
if (getContext() === "background") {
|
||||
getProfile()
|
||||
}
|
||||
config.addChangeListener("profiledir", (prev, cur) => {
|
||||
config.addChangeListener("profiledir", () => {
|
||||
cachedProfile = undefined
|
||||
getProfile()
|
||||
})
|
||||
|
|
|
@ -6,10 +6,12 @@ import * as R from "ramda"
|
|||
export function getPlatformOs(): browser.runtime.PlatformOs {
|
||||
const platform = navigator.platform
|
||||
const mapping = {
|
||||
"win": "Win",
|
||||
"openbsd": "BSD",
|
||||
"mac": "Mac",
|
||||
"linux": "Linux",
|
||||
win: "Win",
|
||||
openbsd: "BSD",
|
||||
mac: "Mac",
|
||||
linux: "Linux",
|
||||
}
|
||||
return R.keys(R.filter(x=>platform.includes(x), mapping))[0] as keyof typeof mapping
|
||||
return R.keys(
|
||||
R.filter(x => platform.includes(x), mapping),
|
||||
)[0] as keyof typeof mapping
|
||||
}
|
||||
|
|
|
@ -5,8 +5,7 @@ const logger = new Logger("requests")
|
|||
|
||||
class DefaultMap<K, V> extends Map<K, V> {
|
||||
constructor(private defaultFactory, ...args) {
|
||||
// super(...args)
|
||||
super()
|
||||
super(...args)
|
||||
}
|
||||
|
||||
get(key) {
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
|
||||
*/
|
||||
|
||||
import * as RssParser from "rss-parser"
|
||||
import * as SemverCompare from "semver-compare"
|
||||
import SemverCompare from "semver-compare"
|
||||
import * as Config from "@src/lib/config"
|
||||
import * as Logging from "@src/lib/logging"
|
||||
import { getTriVersion } from "@src/lib/webext"
|
||||
|
@ -21,47 +20,39 @@ interface TriVersionFeedItem {
|
|||
version: string
|
||||
}
|
||||
|
||||
// initialize to beginning of time to cause a check on startup
|
||||
let highestKnownVersion: TriVersionFeedItem
|
||||
|
||||
function secondsSinceLastCheck() {
|
||||
export function secondsSinceLastCheck() {
|
||||
const lastCheck = Config.get("update", "lastchecktime")
|
||||
return (Date.now() - lastCheck) / 1000
|
||||
}
|
||||
|
||||
// Get the latest version, with a bit of a cache. This will return
|
||||
// immediately if we've already recently checked for an update, so it
|
||||
// should be safe to invoke it relatively frequently.
|
||||
export async function getLatestVersion(force_check = false) {
|
||||
const pastUpdateInterval =
|
||||
secondsSinceLastCheck() > Config.get("update", "checkintervalsecs")
|
||||
if (force_check || pastUpdateInterval) {
|
||||
await updateVersion()
|
||||
}
|
||||
|
||||
return highestKnownVersion
|
||||
}
|
||||
|
||||
async function updateVersion() {
|
||||
// Ask GitHub what the latest version is
|
||||
// Uncached so don't run this often
|
||||
export async function getLatestVersion() {
|
||||
try {
|
||||
// If any monster any makes a novelty tag this will break.
|
||||
// So let's just ignore any errors.
|
||||
const parser = new RssParser()
|
||||
const feed = await parser.parseURL(
|
||||
"https://github.com/tridactyl/tridactyl/tags.atom",
|
||||
const feed = new DOMParser().parseFromString(
|
||||
await (
|
||||
await fetch("https://github.com/tridactyl/tridactyl/tags.atom")
|
||||
).text(),
|
||||
"application/xml",
|
||||
)
|
||||
const mostRecent = feed.items[0]
|
||||
const mostRecent = feed.querySelectorAll("entry")[0]
|
||||
|
||||
// Update our last update check timestamp and the version itself.
|
||||
Config.set("update", "lastchecktime", Date.now())
|
||||
highestKnownVersion = {
|
||||
version: mostRecent.title,
|
||||
releaseDate: new Date(mostRecent.pubDate), // e.g. 2018-12-04T15:24:43.000Z
|
||||
const highestKnownVersion = {
|
||||
version: mostRecent.querySelector("title").textContent,
|
||||
releaseDate: new Date(
|
||||
mostRecent.querySelector("updated").textContent,
|
||||
), // e.g. 2018-12-04T15:24:43.000Z
|
||||
}
|
||||
logger.debug(
|
||||
"Checked for new version of Tridactyl, found ",
|
||||
highestKnownVersion,
|
||||
)
|
||||
|
||||
return highestKnownVersion
|
||||
} catch (e) {
|
||||
logger.error("Error while checking for updates: ", e)
|
||||
}
|
||||
|
|
|
@ -417,3 +417,16 @@ export function interpolateSearchItem(urlPattern: URL, query: string): URL {
|
|||
return new URL(urlPattern.href + query)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url May be either an absolute or a relative URL.
|
||||
* @param baseURI The URL the absolute URL should be relative to. This is
|
||||
* usually the URL of the current page.
|
||||
*/
|
||||
export function getAbsoluteURL(url: string, baseURI: string = document.baseURI) {
|
||||
// We can choose between using complicated RegEx and string manipulation,
|
||||
// or just letting the browser do it for us. The latter is probably safer,
|
||||
// which should make it worth the (small) overhead of constructing an URL
|
||||
// just for this.
|
||||
return new URL(url, baseURI).href
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as convert from "@src/lib/convert"
|
|||
import browserProxy from "@src/lib/browser_proxy"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as UrlUtil from "@src/lib/url_util"
|
||||
import { sleep } from "@src/lib/patience"
|
||||
|
||||
export function inContentScript() {
|
||||
return getContext() === "content"
|
||||
|
@ -9,7 +10,12 @@ export function inContentScript() {
|
|||
|
||||
export function getTriVersion() {
|
||||
const manifest = browser.runtime.getManifest()
|
||||
return manifest.version_name
|
||||
|
||||
// version_name only really exists in Chrome
|
||||
// but we're using it anyway for our own purposes
|
||||
return (manifest as browser._manifest.WebExtensionManifest & {
|
||||
version_name: string
|
||||
}).version_name
|
||||
}
|
||||
|
||||
export function getPrettyTriVersion() {
|
||||
|
@ -89,6 +95,22 @@ export async function ownTabId() {
|
|||
return (await ownTab()).id
|
||||
}
|
||||
|
||||
async function windows() {
|
||||
return (await browserBg.windows.getAll())
|
||||
.map(w => w.id)
|
||||
.sort((a, b) => a - b)
|
||||
}
|
||||
|
||||
/* Returns Tridactyl's window index. */
|
||||
export async function ownWinTriIndex() {
|
||||
return (await windows()).indexOf((await ownTab()).windowId)
|
||||
}
|
||||
|
||||
/* Returns mozilla's internal window id from Tridactyl's index. */
|
||||
export async function getWinIdFromIndex(index: string) {
|
||||
return (await windows())[index]
|
||||
}
|
||||
|
||||
export async function ownTabContainer() {
|
||||
return browserBg.contextualIdentities.get((await ownTab()).cookieStoreId)
|
||||
}
|
||||
|
@ -129,6 +151,7 @@ export async function openInNewTab(
|
|||
related: false,
|
||||
cookieStoreId: undefined,
|
||||
},
|
||||
waitForDOM = false,
|
||||
) {
|
||||
const thisTab = await activeTab()
|
||||
const options: Parameters<typeof browser.tabs.create>[0] = {
|
||||
|
@ -164,14 +187,43 @@ export async function openInNewTab(
|
|||
break
|
||||
}
|
||||
|
||||
const tabCreateWrapper = async options => {
|
||||
const tab = await browserBg.tabs.create(options)
|
||||
const answer: Promise<browser.tabs.Tab> = new Promise(resolve => {
|
||||
// This can't run in content scripts, obviously
|
||||
// surely we never call this from a content script?
|
||||
if (waitForDOM) {
|
||||
const listener = (message, sender) => {
|
||||
if (
|
||||
message === "dom_loaded_background" &&
|
||||
sender?.tab?.id === tab.id
|
||||
) {
|
||||
browserBg.runtime.onMessage.removeListener(listener)
|
||||
resolve(tab)
|
||||
}
|
||||
}
|
||||
browserBg.runtime.onMessage.addListener(listener)
|
||||
} else {
|
||||
resolve(tab)
|
||||
}
|
||||
})
|
||||
// Return on slow- / extremely quick- loading pages anyway
|
||||
return Promise.race([
|
||||
answer,
|
||||
(async () => {
|
||||
await sleep(750)
|
||||
return tab
|
||||
})(),
|
||||
])
|
||||
}
|
||||
if (kwargs.active === false) {
|
||||
// load in background
|
||||
return browserBg.tabs.create(options)
|
||||
return tabCreateWrapper(options)
|
||||
} else {
|
||||
// load in background and then activate, per issue #1993
|
||||
return browserBg.tabs
|
||||
.create(options)
|
||||
.then(newtab => browserBg.tabs.update(newtab.id, { active: true }))
|
||||
return tabCreateWrapper(options).then(newtab =>
|
||||
browserBg.tabs.update(newtab.id, { active: true }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,7 +289,7 @@ export async function queryAndURLwrangler(
|
|||
try {
|
||||
const url = new URL("http://" + address)
|
||||
// Ignore unlikely domains
|
||||
if (url.hostname.includes(".") || url.port || url.password) {
|
||||
if (url.hostname.indexOf(".") > 0 || url.port || url.password) {
|
||||
return url.href
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Tridactyl",
|
||||
"version": "1.20.1",
|
||||
"version_name": "1.20.1",
|
||||
"version": "1.21.1",
|
||||
"version_name": "1.21.1",
|
||||
"icons": {
|
||||
"64": "static/logo/Tridactyl_64px.png",
|
||||
"100": "static/logo/Tridactyl_100px.png",
|
||||
|
|
|
@ -154,11 +154,11 @@ export function listenForCounters(
|
|||
observer: PerformanceObserver,
|
||||
) => void
|
||||
if (statsLogger === undefined) {
|
||||
callback = (list, observer) => {
|
||||
callback = (list) => {
|
||||
sendStats(list.getEntries())
|
||||
}
|
||||
} else {
|
||||
callback = (list, observer) => {
|
||||
callback = (list) => {
|
||||
statsLogger.pushList(list.getEntries())
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ export class StatsLogger {
|
|||
return this.buffer.filter(filterFun)
|
||||
}
|
||||
|
||||
private updateBuffersize() {
|
||||
public updateBuffersize() {
|
||||
// Changing the buffer length while this is running will
|
||||
// probably result in weirdness, but that shouldn't be a major
|
||||
// issue - it's not like we need these to be in order or
|
||||
|
|
|
@ -91,7 +91,9 @@ const state = new Proxy(overlay, {
|
|||
},
|
||||
})
|
||||
|
||||
export async function getAsync(property) {
|
||||
export async function getAsync<K extends keyof State>(
|
||||
property: K,
|
||||
): Promise<State[K]> {
|
||||
if (notBackground())
|
||||
return browser.runtime.sendMessage({
|
||||
type: "state",
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="110"
|
||||
height="20"
|
||||
version="1.1"
|
||||
id="svg28"
|
||||
sodipodi:docname="freenode-badge.svg"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
|
||||
<metadata
|
||||
id="metadata34">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs32" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1356"
|
||||
inkscape:window-height="883"
|
||||
id="namedview30"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.5636364"
|
||||
inkscape:cx="55"
|
||||
inkscape:cy="10"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="176"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg28" />
|
||||
<linearGradient
|
||||
id="b"
|
||||
x2="0"
|
||||
y2="100%">
|
||||
<stop
|
||||
offset="0"
|
||||
stop-color="#bbb"
|
||||
stop-opacity=".1"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-opacity=".1"
|
||||
id="stop4" />
|
||||
</linearGradient>
|
||||
<clipPath
|
||||
id="a">
|
||||
<rect
|
||||
width="110"
|
||||
height="20"
|
||||
rx="3"
|
||||
fill="#fff"
|
||||
id="rect7" />
|
||||
</clipPath>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
id="g16">
|
||||
<path
|
||||
fill="#555"
|
||||
d="M0 0h33v20H0z"
|
||||
id="path10" />
|
||||
<path
|
||||
d="m 33,0 h 77 V 20 H 33 Z"
|
||||
id="path12"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#46bc99" />
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M0 0h110v20H0z"
|
||||
id="path14" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
|
||||
font-size="110"
|
||||
id="g26">
|
||||
<text
|
||||
x="175"
|
||||
y="150"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="230"
|
||||
id="text18">chat</text>
|
||||
<text
|
||||
x="175"
|
||||
y="140"
|
||||
transform="scale(.1)"
|
||||
textLength="230"
|
||||
id="text20">chat</text>
|
||||
<text
|
||||
x="705"
|
||||
y="150"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="670"
|
||||
id="text22">on freenode</text>
|
||||
<text
|
||||
x="705"
|
||||
y="140"
|
||||
transform="scale(.1)"
|
||||
textLength="670"
|
||||
id="text24">on freenode</text>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.9 KiB |
1
src/static/badges/libera-badge.svg
Normal file
1
src/static/badges/libera-badge.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="94" height="20" role="img" aria-label="chat: on Libera"><iframe xmlns="http://www.w3.org/1999/xhtml" class="cleanslate hidden" src="libera-badge_files/commandline.html" id="cmdline_iframe" loading="lazy" style="height: 0px !important;"></iframe><title xmlns="http://www.w3.org/2000/svg">chat: on Libera</title><linearGradient xmlns="http://www.w3.org/2000/svg" id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath xmlns="http://www.w3.org/2000/svg" id="r"><rect width="94" height="20" rx="3" fill="#fff"/></clipPath><g xmlns="http://www.w3.org/2000/svg" clip-path="url(#r)"><rect width="33" height="20" fill="#555"/><rect x="33" width="61" height="20" fill="#ff55dd"/><rect width="94" height="20" fill="url(#s)"/></g><g xmlns="http://www.w3.org/2000/svg" fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="175" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="230">chat</text><text x="175" y="140" transform="scale(.1)" fill="#fff" textLength="230">chat</text><text aria-hidden="true" x="625" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">on Libera</text><text x="625" y="140" transform="scale(.1)" fill="#fff" textLength="510">on Libera</text></g></a0:svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -10,7 +10,7 @@ It will not cover advanced topics. For those, [`:help`](../docs/modules/_src_exc
|
|||
|
||||
## Basics
|
||||
|
||||
The idea behind Tridactyl is to allow you to navigate the web more efficiently with just the keyboard. Tridactyl turns Firefox into a modal browser, meaning it has several different modes of operation, like Vim. Each tab can only ever be in one mode at a time and each of these modes could have a wildly different operation. You can think of it a bit like a Transformer, if you like. There are four main modes you will want to be familiar with:
|
||||
The idea behind Tridactyl is to allow you to navigate the web more efficiently with just the keyboard. Tridactyl turns Firefox into a modal browser, meaning it has several different modes of operation, like Vim. Each tab can only ever be in one mode at a time and each of these modes could have a wildly different operation. You can think of it a bit like a Transformer, if you like. There are five main modes you will want to be familiar with:
|
||||
|
||||
- Normal mode
|
||||
- This mode is used for navigating around single pages and starting other modes.
|
||||
|
@ -20,6 +20,9 @@ The idea behind Tridactyl is to allow you to navigate the web more efficiently w
|
|||
- This is most often used for following links, but it has many other submodes.
|
||||
- You can enter this mode with `f` and exit it with `Escape` or `Enter`.
|
||||
- Hint characters are displayed as uppercase letters, but you should type the lowercase letter.
|
||||
- Visual mode (experimental)
|
||||
- This mode allows you to select text on the web page and copy it to the clipboard or search for it using `s` and `S`.
|
||||
- You can enter this mode with `v`, by selecting text with the mouse, `;h` hint mode, `/` searching or by using Firefox's "caret" mode on `F7` and exit it with `Escape` or `Ctrl-[`.
|
||||
- Command mode ("ex-mode")
|
||||
- This mode allows you to execute more complicated commands by typing them out manually.
|
||||
- It is commonly used for binding keys and accessing help.
|
||||
|
@ -28,6 +31,7 @@ The idea behind Tridactyl is to allow you to navigate the web more efficiently w
|
|||
- This mode passes all keypresses through to the web page. It is useful for websites that have their own keybinds, such as games and Gmail.
|
||||
- You can toggle the mode with `Shift-Insert`, `Ctrl-Alt-Escape`, `Ctrl-Alt-Backtick`, or `Shift-Esc`.
|
||||
- While in ignore mode, you can execute a single normal mode binding by pressing `<C-o>` followed by the keys for the binding.
|
||||
- Tridactyl can be configured to enter ignore mode for specified URLs. Run `:blacklistadd [url]` to put Tridactyl in ignore mode on the provided URL. Use the command `:blacklistremove [url]` to remove the URL from the blacklist.
|
||||
|
||||
Almost all of the modes are controlled by series of keypresses. In this tutorial, a sequence of keys such as `zz` should be entered by pressing the key `z`, letting go, and then pressing the key `z`. There is no need to hold both keys at once, if that were even possible. (`zz` resets the zoom level to the default, so it probably didn't seem to do anything). Sometimes `help` refers to a command that must be entered in command mode; it should hopefully always be clear from context which we mean.
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ Use `.` to repeat the last action.
|
|||
|
||||
Many keypresses in normal mode take you into another mode. `t`, for example, puts you into command mode prefilled with the command for opening a new tab; `F` puts you into hinting mode to open a link in a background tab; `gi` focuses the first text box on the page and allows you to press `Tab` to switch between text boxes.
|
||||
|
||||
Tridactyl uses a similar notation to Vim for keys with modifiers: `<C-x>` means press Ctrl, tap x, release Ctrl; i.e. `Ctrl-x`. `<C-` almost always means a literal Ctrl key, even on Macs: `<M-` means the Meta ("splat") key. The exception is when describing default Firefox binds for Macintosh: then `<C-` means Meta. This arises most often for find mode where the `<C-g>`, `<C-G>` and `<C-f>` default binds are all in fact `<M-g>`, `<M-G>` and `<M-f>`.
|
||||
|
||||
## Useful normal mode keybinds
|
||||
|
||||
- `b` brings up a list of your current tabs. Press `Tab`/`Shift-Tab` to cycle through them and enter to select. You can also type to filter down the tabs based on their titles and URLs
|
||||
|
@ -18,14 +20,14 @@ Many keypresses in normal mode take you into another mode. `t`, for example, put
|
|||
- `o` in the current tab
|
||||
- `t` in a new tab
|
||||
- Using a capital letter in place of any of the previous commands opens the command with the current URL pasted into it, i.e, `W`,`O`,`T`
|
||||
- `s` lets you search easily
|
||||
- `s` lets you search easily in the current tab and `S` does so in a new tab.
|
||||
- in general, you can search many search engines straight from these prompts by simply starting your query with the search engine, such as `bing` `duckduckgo` or `scholar`
|
||||
- Navigate history with `H` and `L`
|
||||
- `yy` copies the current URL to your clipboard
|
||||
- `p` opens the clipboard contents as a web page, or searches for it, in the current tab. `P` opens it in a new tab
|
||||
- Protip: quickly search for the source of a quote by using `;p` to copy a paragraph, and `P` to search the internet for it
|
||||
- `zi`,`zo`,`zz` zoom in, out and return to the default zoom
|
||||
- Search text with Firefox's standard `/` binding, jump from match to match with `<C-g>` or `<C-G>` (note that it isn't possible to rebind searching/jumping between matches for now). If you want to use Firefox's `<C-f>` search you'll have to run `unbind <C-f>`.
|
||||
- Search text with Firefox's standard `/` binding, jump from match to match with `<C-g>` or `<C-G>` (note that it isn't possible to rebind searching/jumping between matches for now). If you want to use Firefox's `<C-f>` search you'll have to run `unbind <C-f>` (unless you're on a Mac where Firefox's find mode is bound to `<M-f>`).
|
||||
|
||||
* `<C-v>` sends the next keystroke to the current website, bypassing bindings
|
||||
|
||||
|
|
25
src/static/clippy/3-1-visual_mode.md
Normal file
25
src/static/clippy/3-1-visual_mode.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Visual mode (experimental)
|
||||
|
||||
"visual" mode is by default entered whenever non-text-area text is selected and left whenever it is deselected. It can be manually entered with
|
||||
- `v`
|
||||
- by using the mouse to select text
|
||||
- by selecting text with `;h` hint mode
|
||||
- by searching with `/`
|
||||
- using Firefox's "caret" mode on `F7`
|
||||
|
||||
The default behaviour can be modified with `:set visual{enter,exit}auto {true,false}`.
|
||||
|
||||
The visual mode keybinds:
|
||||
|
||||
- Text selection using common movement keys:
|
||||
- `h`,`j`,`k`,`l`,`e`,`w`,`b` moves the cursor and expands or reduces the selection
|
||||
- `0` expands or reduces the selection to the beginning of the line
|
||||
- `$` expands or reduces the selection to the end of the line
|
||||
- `=` expands the selection successively until the whole web page is selected
|
||||
- `o` moves the (invisible) cursor to the other end of the selection
|
||||
- `y` yanks selected text to the clipboard
|
||||
- `s` and `S` searches for selected text
|
||||
|
||||
See ":help vmaps" to see all the binds.
|
||||
|
||||
The [next page](./4-command_mode.html) will cover the command mode. <a href='./3-hint_mode.html' rel="prev"></a>
|
|
@ -9,9 +9,10 @@ Here are some of the most useful hint modes:
|
|||
- `:hint -p` or `;p`: copy element text (such as a paragraph) to clipboard
|
||||
- `:hint -#` or `;#`: copy anchor location. Useful for linking someone to a specific part of a page.
|
||||
- `:hint -k` or `;k`: kill an element. Very satisfying.
|
||||
- `:hint -K` or `;K`: kill an element. Killed elements can be restored with :elementunhide
|
||||
|
||||
If there is ever only a single hint remaining (for example, because you have wittled them down, or there is only a single link visible on the page) the hint mode will follow it automatically.
|
||||
|
||||
Some hints have their tags (the labels which show which characters to press to activate them) in grey in the default theme. These correspond to elements which have JavaScript mouse events attached to them. If an element has a grey and a red hint tag next to it, pick the red one as this is almost always the correct tag.
|
||||
|
||||
The [next page](./4-command_mode.html) will cover the command mode. <a href='./2-normal_mode.html' rel="prev"></a>
|
||||
The [next page](./3-1-visual_mode.html) will cover the visual mode. <a href='./2-normal_mode.html' rel="prev"></a>
|
||||
|
|
|
@ -2,20 +2,22 @@
|
|||
|
||||
Command mode, i.e, "the console", is used for accessing less frequently used commands, such as:
|
||||
|
||||
* `tabdetach` to detach the current tab into a new window
|
||||
* `bind [key] [excommand]` to bind keys
|
||||
* `viewsource` to view the current page's source
|
||||
* `viewconfig nmaps` to view the current normal mode bindings
|
||||
* `help [command]` to access help on a command
|
||||
* `help [key]` to access help on an existing binding
|
||||
* `composite [command 1]; [command 2]; [command 3]...` lets you execute commands sequentially, useful for binding. If you want the results of each command to be piped to the other, use pipes `|` instead of semi-colons.
|
||||
- `tabdetach` to detach the current tab into a new window
|
||||
- `bind [key] [excommand]` to bind keys
|
||||
- `viewsource` to view the current page's source
|
||||
- `viewconfig nmaps` to view the current normal mode bindings
|
||||
- `help [command]` to access help on a command
|
||||
- `help [key]` to access help on an existing binding
|
||||
- `composite [command 1]; [command 2]; [command 3]...` lets you execute commands sequentially, useful for binding. If you want the results of each command to be piped to the other, use pipes `|` instead of semi-colons.
|
||||
|
||||
We support a handful of keybinds in the console:
|
||||
|
||||
* `Ctrl-C` to exit to normal mode, or copy selected text.
|
||||
* `Up`/`Down` to search in command history: e.g. `:tabopen` `Up` will show you the most recent tabopen command.
|
||||
* `Tab`/`Shift-Tab` cycle completion, enter to select
|
||||
* `Ctrl-F` to complete the command from command history
|
||||
* `Space` to insert the URL of the highlighted completion into the command line
|
||||
- `Up`/`Down` to search in command history: e.g. `:tabopen` `Up` will show you the most recent tabopen command.
|
||||
- `Tab`/`Shift-Tab` cycle completion, enter to select
|
||||
- `Ctrl-F` to complete the command from command history
|
||||
- `Space` to insert the URL of the highlighted completion into the command line
|
||||
- `Shift-Delete` to close the highlighted completion on `:tab` and `:taball` completions
|
||||
- `Ctrl-Enter` to execute the highlighted completion and keep the command line open
|
||||
- `<C-o>yy` to copy the highlighted completion to your clipboard
|
||||
|
||||
The [next page](./5-settings.html) will talk about the various settings available. <a href='./3-hint_mode.html' rel="prev"></a>
|
||||
The [next page](./5-settings.html) will talk about the various settings available. <a href='./3-1-visual_mode.html' rel="prev"></a>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue