diff --git a/.eslintignore b/.eslintignore index 2b7d70f9..34b26177 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,5 @@ build coverage *.test.ts test_utils.ts +e2e_tests +compiler diff --git a/.eslintrc.js b/.eslintrc.js index aa8d3a7c..ece2ab1a 100644 --- a/.eslintrc.js +++ b/.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", diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fd5be967..a2018ef1 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -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 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d64915ff..8160695e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 84994b24..c1a7dab3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ compiler/*.js compiler/**/*.js .*.generated.ts .tmp/ +.DS_Store +.build_cache/ +yarn-error.log diff --git a/.tridactylrc b/.tridactylrc index 8e0580b8..93a022b5 100644 --- a/.tridactylrc +++ b/.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 `` 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 - -" Allow Ctrl-c to copy in the commandline -unbind --mode=ex - -" 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:[""],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 `` 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 +" +" " Allow Ctrl-c to copy in the commandline +" unbind --mode=ex +" +" " 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:[""],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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 005333a4..27eafde9 100644 --- a/CHANGELOG.md +++ b/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 `` 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` 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)) + - `` and `` 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)) + - `` 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 `` 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 diff --git a/ci/lint.sh b/ci/lint.sh index 10224bce..8758e0c8 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -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 . diff --git a/ci/mozilla.sh b/ci/mozilla.sh index 5d89d437..e8874674 100755 --- a/ci/mozilla.sh +++ b/ci/mozilla.sh @@ -1,4 +1,4 @@ #! /bin/sh yarn run build --no-native -cd ${0%/*}/../build +cd "${0%/*}"/../build || exit 1 "$(yarn bin)/web-ext" lint diff --git a/ci/unit.sh b/ci/unit.sh index 5104b53f..3c878270 100755 --- a/ci/unit.sh +++ b/ci/unit.sh @@ -1,4 +1,4 @@ #! /bin/sh -cd ${0%/*} +cd "${0%/*}" || exit yarn run build --no-native "$(yarn bin)/jest" src diff --git a/contributing.md b/contributing.md index 7860e924..e3033d50 100644 --- a/contributing.md +++ b/contributing.md @@ -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 diff --git a/doc/newsletters/05-autumn-2020.md b/doc/newsletters/05-autumn-2020.md new file mode 100644 index 00000000..a79606b5 --- /dev/null +++ b/doc/newsletters/05-autumn-2020.md @@ -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 `` and `` was hard to beat (I've personally switched to using `` instead of `` 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: `yy` copies the currently selected completion to the clipboard, `` executes `:tabclose` on a selected completion's arguments and `` 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 `` 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. diff --git a/doc/newsletters/06-winter-2021.md b/doc/newsletters/06-winter-2021.md new file mode 100644 index 00000000..448459dd --- /dev/null +++ b/doc/newsletters/06-winter-2021.md @@ -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 `` 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`. + +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 diff --git a/doc/newsletters/07-spring-2021.md b/doc/newsletters/07-spring-2021.md new file mode 100644 index 00000000..ed5701ba --- /dev/null +++ b/doc/newsletters/07-spring-2021.md @@ -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 `` 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. `>:(` diff --git a/doc/newsletters/08-summer-2021.md b/doc/newsletters/08-summer-2021.md new file mode 100644 index 00000000..532028c1 --- /dev/null +++ b/doc/newsletters/08-summer-2021.md @@ -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 diff --git a/doc/newsletters/tips-and-tricks/1-hint-css-selectors.md b/doc/newsletters/tips-and-tricks/1-hint-css-selectors.md new file mode 100644 index 00000000..9f6be46e --- /dev/null +++ b/doc/newsletters/tips-and-tricks/1-hint-css-selectors.md @@ -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"] +``` + +![Left: full hints on a Google result page. Right: custom hints which only highlight results](https://raw.githubusercontent.com/tridactyl/tridactyl/606cfc581364ce09d7d197263506f544e8d1d470/doc/newsletters/tips-and-tricks/assets/1-google-hint-comparison.png) + +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 `

` 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. `

` 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 `` 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: + +``` +

+ + James Humphreys + +... +

  • +::marker + + Hurricane Iota + +... +``` + +![Wikipedia's main page with the Firefox developer tools open](https://raw.githubusercontent.com/tridactyl/tridactyl/606cfc581364ce09d7d197263506f544e8d1d470/doc/newsletters/tips-and-tricks/assets/1-wiki-inspect-element.png) + +It looks like the links I want to hint are always directly enclosed by a `` (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! + +![Left: full hints on the Wikipedia homepage. Right: custom hints which only highlight major articles](https://raw.githubusercontent.com/tridactyl/tridactyl/606cfc581364ce09d7d197263506f544e8d1d470/doc/newsletters/tips-and-tricks/assets/1-wiki-hint-comparison.png) + +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: +1: +... +55: +56: +``` + +![The Firefox developer console after running the query selector](https://raw.githubusercontent.com/tridactyl/tridactyl/606cfc581364ce09d7d197263506f544e8d1d470/doc/newsletters/tips-and-tricks/assets/1-wiki-console.png) + +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 diff --git a/doc/troubleshooting.md b/doc/troubleshooting.md index 54096357..f92963a1 100644 --- a/doc/troubleshooting.md +++ b/doc/troubleshooting.md @@ -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". diff --git a/e2e_tests/utils.ts b/e2e_tests/utils.ts index 80456a9c..6b0d5ab1 100644 --- a/e2e_tests/utils.ts +++ b/e2e_tests/utils.ts @@ -54,7 +54,7 @@ const modToSelenium = { } export function sendKeys (driver, keys) { - const delay = 300 + const delay = 500 function chainRegularKeys (previousPromise, regularKeys) { return regularKeys .split("") diff --git a/hooks/post-checkout b/hooks/post-checkout index b1198053..7103d9e5 100755 --- a/hooks/post-checkout +++ b/hooks/post-checkout @@ -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)" diff --git a/hooks/post-merge b/hooks/post-merge index 52765ed5..9072393d 100755 --- a/hooks/post-merge +++ b/hooks/post-merge @@ -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)" diff --git a/hooks/pre-commit b/hooks/pre-commit index 461a9c9a..36416564 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -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) diff --git a/issue_template.md b/issue_template.md index 894642fc..36cc4015 100644 --- a/issue_template.md +++ b/issue_template.md @@ -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 -* 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: diff --git a/jest.config.js b/jest.config.js index ad6bb8c2..d967fda7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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] diff --git a/native/README.md b/native/README.md new file mode 100644 index 00000000..b539d6f0 --- /dev/null +++ b/native/README.md @@ -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 diff --git a/native/current_native_version b/native/current_native_version new file mode 100644 index 00000000..449d7e73 --- /dev/null +++ b/native/current_native_version @@ -0,0 +1 @@ +0.3.6 diff --git a/native/install.sh b/native/install.sh index 49fdb413..7a363bb5 100755 --- a/native/install.sh +++ b/native/install.sh @@ -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." diff --git a/package.json b/package.json index 8e4c3824..5fefe014 100644 --- a/package.json +++ b/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": { diff --git a/privacy.md b/privacy.md new file mode 100644 index 00000000..f769b85f --- /dev/null +++ b/privacy.md @@ -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. diff --git a/readme.md b/readme.md index 71eb7672..09afaf6c 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,39 @@ -![Tridactyl logo](src/static/logo/Tridactyl_200px.png) +

    +
    +Tridactyl Logo +
    +Tridactyl +
    +

    -# Tridactyl [![Build Status](https://travis-ci.org/tridactyl/tridactyl.svg?branch=master)](https://travis-ci.org/tridactyl/tridactyl) [![Matrix Chat][matrix-badge]][matrix-link] [![Gitter Chat][gitter-badge]][gitter-link] +

    Replace Firefox's default control mechanism with one modelled on the one true editor, Vim.

    -Replace Firefox's default control mechanism with one modelled on the one true editor, Vim. +

    +Build Status +Matrix Chat +Join Gitter Chat +Join Discord Chat +Mozilla Addon Store +

    -![Gigantic GIF showing Tridactyl in action](doc/AMO_screenshots/trishowcase.gif) +

    +Installation • +Changelog • +First Look • +Features • +FAQ • +Contributing +

    -## Installing +
    -[Simply click this link in Firefox to install our latest "beta" build][riskyclick]. If you want more options, read on. +

    +Tridactyl GIF +

    + +## 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 - ``/`` — 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 `` — open the Find in page search box - ``/`` — find the next/previous instance of the last find operation (note: these are the standard Firefox shortcuts) -Please note that Tridactyl overrides Firefox's `` search, replacing it with a binding to go to the next part of the page. If you want to be able to use `` again to search for things, use `unbind `. +Please note that Tridactyl overrides Firefox's `` search, replacing it with a binding to go to the next part of the page. +If you want to be able to use `` to search for things, use `` 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 `` at any time, use `unbind ` to unset the scrollpage binding. #### Bookmarks and quickmarks @@ -115,6 +143,7 @@ If you want to use Firefox's default `` 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 - `` — 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 `. 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 you'll find that you'll probably need to press Control+Shift+6 to trigger it. The default bind is which does not require you to press shift. + If you want to bind `` you'll find that you'll probably need to press Control+Shift+6 to trigger it. The default bind is `` 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. PayPal -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 - - ### 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 www.Vecteezy.com -[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 diff --git a/scripts/authors.sh b/scripts/authors.sh index d0d9ead4..415d1d84 100755 --- a/scripts/authors.sh +++ b/scripts/authors.sh @@ -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/^/

    /' | 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/^/

    /' | sed 's/$/<\/p>/' > ../../.build_cache/authors +fi + +cat ../../.build_cache/authors >> "$authors" + sed "1,/REPLACETHIS/ d" authors.html >> "$authors" diff --git a/scripts/bodgecss.sh b/scripts/bodgecss.sh deleted file mode 100755 index 83ef511c..00000000 --- a/scripts/bodgecss.sh +++ /dev/null @@ -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 diff --git a/scripts/build.sh b/scripts/build.sh index 2c0e687c..7f43e24c 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -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 diff --git a/scripts/common.sh b/scripts/common.sh index 63b3e3ae..39ec1383 100755 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -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")" diff --git a/scripts/esbuild.js b/scripts/esbuild.js new file mode 100644 index 00000000..9846422d --- /dev/null +++ b/scripts/esbuild.js @@ -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)) +} diff --git a/scripts/sign b/scripts/sign index 194c3187..103ce11b 100755 --- a/scripts/sign +++ b/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 diff --git a/scripts/version.js b/scripts/version.js index d153ab1b..301e1aaf 100755 --- a/scripts/version.js +++ b/scripts/version.js @@ -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 diff --git a/src/background.ts b/src/background.ts index a02962f8..d58a83c6 100644 --- a/src/background.ts +++ b/src/background.ts @@ -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) => 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() diff --git a/src/background/commands.ts b/src/background/commands.ts index c450d09a..f9c3b466 100644 --- a/src/background/commands.ts +++ b/src/background/commands.ts @@ -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) { return (command_name: string) => { const command = commands.filter(c => c.name == command_name)[0] const exstring = config.get( diff --git a/src/background/config_rc.ts b/src/background/config_rc.ts index 263b4af8..5b86da78 100644 --- a/src/background/config_rc.ts +++ b/src/background/config_rc.ts @@ -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("#"), + ) } diff --git a/src/background/download_background.ts b/src/background/download_background.ts index f48fcc0c..1b442e42 100644 --- a/src/background/download_background.ts +++ b/src/background/download_background.ts @@ -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( diff --git a/src/background/editor.ts b/src/background/editor.ts index 5e9ecb7d..d8e3bb59 100644 --- a/src/background/editor.ts +++ b/src/background/editor.ts @@ -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 extends (elem, ...args: infer U) => any ? U : never diff --git a/src/background/meta.ts b/src/background/meta.ts new file mode 100644 index 00000000..707e72a6 --- /dev/null +++ b/src/background/meta.ts @@ -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 +} diff --git a/src/background/omnibox.ts b/src/background/omnibox.ts index 70a94432..f1f56880 100644 --- a/src/background/omnibox.ts +++ b/src/background/omnibox.ts @@ -6,7 +6,6 @@ import * as controller from "@src/lib/controller" export function inputEnteredListener( input: string, - disposition: browser.omnibox.OnInputEnteredDisposition, ) { controller.acceptExCmd(input) } diff --git a/src/background/user_actions.ts b/src/background/user_actions.ts index 8f734ca4..9cd7defa 100644 --- a/src/background/user_actions.ts +++ b/src/background/user_actions.ts @@ -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 void> = { + escapehatch, +} diff --git a/src/commandline_frame.ts b/src/commandline_frame.ts index 2c58bedd..be054234 100644 --- a/src/commandline_frame.ts +++ b/src/commandline_frame.ts @@ -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, 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[] = [(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 = 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 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", diff --git a/src/completions.ts b/src/completions.ts index 674f7b9d..e939b9d4 100644 --- a/src/completions.ts +++ b/src/completions.ts @@ -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 - abstract async next(inc?: number): Promise + abstract next(inc?: number): Promise } // 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`

    ` */ - - 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) {} } diff --git a/src/completions/Bmark.ts b/src/completions/Bmark.ts index 24bdda8f..df5441fd 100644 --- a/src/completions/Bmark.ts +++ b/src/completions/Bmark.ts @@ -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() diff --git a/src/completions/Composite.ts b/src/completions/Composite.ts index ba202472..1ddd999d 100644 --- a/src/completions/Composite.ts +++ b/src/completions/Composite.ts @@ -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" diff --git a/src/completions/Excmd.ts b/src/completions/Excmd.ts index 05ba404b..ada12fa7 100644 --- a/src/completions/Excmd.ts +++ b/src/completions/Excmd.ts @@ -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" diff --git a/src/completions/Find.ts b/src/completions/Find.ts index 6e7338a1..e7295203 100644 --- a/src/completions/Find.ts +++ b/src/completions/Find.ts @@ -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")) } diff --git a/src/completions/Rss.ts b/src/completions/Rss.ts index 9a9858bd..aa0684f7 100644 --- a/src/completions/Rss.ts +++ b/src/completions/Rss.ts @@ -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) } diff --git a/src/completions/Sessions.ts b/src/completions/Sessions.ts index af4cc182..8042c74e 100644 --- a/src/completions/Sessions.ts +++ b/src/completions/Sessions.ts @@ -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) diff --git a/src/completions/Settings.ts b/src/completions/Settings.ts index 38bd3a80..00148167 100644 --- a/src/completions/Settings.ts +++ b/src/completions/Settings.ts @@ -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(" ") diff --git a/src/completions/Tab.ts b/src/completions/Tab.ts index db4f4c07..0a7e5ac2 100644 --- a/src/completions/Tab.ts +++ b/src/completions/Tab.ts @@ -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` ${pre.padEnd(2)} - - ${tab.index + 1}: ${tab.title} + + ${tab.index + 1}: ${indicator} ${tab.title} ${tab.url} @@ -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 { + private async reactToTabChanges(): Promise { 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)) } } diff --git a/src/completions/TabAll.ts b/src/completions/TabAll.ts index b38365f7..734fe5db 100644 --- a/src/completions/TabAll.ts +++ b/src/completions/TabAll.ts @@ -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 { + // 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 diff --git a/src/completions/Theme.ts b/src/completions/Theme.ts new file mode 100644 index 00000000..578d494d --- /dev/null +++ b/src/completions/Theme.ts @@ -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` + ${value} + ` + } +} + +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 + // }) + } +} diff --git a/src/completions/Window.ts b/src/completions/Window.ts index 1abd6682..8ab949ed 100644 --- a/src/completions/Window.ts +++ b/src/completions/Window.ts @@ -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 = [] diff --git a/src/content.ts b/src/content.ts index 947e6aa0..6adb477d 100644 --- a/src/content.ts +++ b/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 diff --git a/src/content/commandline_content.ts b/src/content/commandline_content.ts index 390c9ffc..e12c3e70 100644 --- a/src/content/commandline_content.ts +++ b/src/content/commandline_content.ts @@ -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(() => { diff --git a/src/content/editor.ts b/src/content/editor.ts index 14e244d9..446040ef 100644 --- a/src/content/editor.ts +++ b/src/content/editor.ts @@ -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) { diff --git a/src/content/finding.ts b/src/content/finding.ts index 0626641f..e3a6a3fb 100644 --- a/src/content/finding.ts +++ b/src/content/finding.ts @@ -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) { diff --git a/src/content/hinting.ts b/src/content/hinting.ts index 94068553..ab1103d2 100644 --- a/src/content/hinting.ts +++ b/src/content/hinting.ts @@ -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 != "", ), ), ), diff --git a/src/content/scrolling.ts b/src/content/scrolling.ts index 5f8296a5..1dc6ea66 100644 --- a/src/content/scrolling.ts +++ b/src/content/scrolling.ts @@ -180,7 +180,6 @@ export async function recursiveScroll( xDistance: number, yDistance: number, node?: Element, - stopAt?: Element, ) { let startingFromCached = false if (!node) { diff --git a/src/content/state_content.ts b/src/content/state_content.ts index d5948c10..98906123 100644 --- a/src/content/state_content.ts +++ b/src/content/state_content.ts @@ -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 = diff --git a/src/content/styling.ts b/src/content/styling.ts index 381194d1..e6a85ea0 100644 --- a/src/content/styling.ts +++ b/src/content/styling.ts @@ -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 diff --git a/src/content/toys.ts b/src/content/toys.ts index 08f3c4b5..2469a35a 100644 --- a/src/content/toys.ts +++ b/src/content/toys.ts @@ -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) } diff --git a/src/excmds.ts b/src/excmds.ts index d459ae72..a84a03bb 100644 --- a/src/excmds.ts +++ b/src/excmds.ts @@ -41,9 +41,9 @@ ## Caveats There are some caveats common to all webextension vimperator-alikes: - - To make Tridactyl work on addons.mozilla.org and some other Mozilla domains, you need to open `about:config`, run [[fixamo]] or add a new boolean `privacy.resistFingerprinting.block_mozAddonManager` with the value `true`, and remove the above domains from `extensions.webextensions.restrictedDomains`. + - To make Tridactyl work on addons.mozilla.org and some other Mozilla domains, you need to open `about:config` and add a new boolean `privacy.resistFingerprinting.block_mozAddonManager` with the value `true`, as well as remove those domains from `extensions.webextensions.restrictedDomains`. - Tridactyl can't run on about:\*, some file:\* URIs, view-source:\*, or data:\*, URIs. - - To change/hide the GUI of Firefox from Tridactyl, you can use [[guiset]] with the native messenger installed (see [[native]] and [[installnative]]). Alternatively, you can edit your userChrome yourself. + - To change/hide the GUI of Firefox from Tridactyl, you can use [[guiset]] with the native messenger installed (see [[native]] and [[nativeinstall]]). Alternatively, you can edit your userChrome yourself. ## Getting help @@ -53,7 +53,7 @@ [![Matrix Chat][matrix-badge]][matrix-link] [![Gitter Chat][gitter-badge]][gitter-link] - [![Freenode Chat][freenode-badge]][freenode-link] + [![Libera Chat][libera-badge]][libera-link] All three channels are mirrored together, so it doesn't matter which one you use. @@ -63,10 +63,10 @@ [gitter-badge]: /static/badges/gitter-badge.svg [gitter-link]: https://gitter.im/tridactyl/Lobby - [freenode-badge]: /static/badges/freenode-badge.svg - [freenode-link]: ircs://chat.freenode.net/tridactyl - [matrix-badge]: https://matrix.to/img/matrix-badge.svg - [matrix-link]: https://riot.im/app/#/room/#tridactyl:matrix.org + [libera-badge]: /static/badges/libera-badge.svg + [libera-link]: ircs://irc.libera.chat:6697/tridactyl + [matrix-badge]: /static/badges/matrix-badge.svg + [matrix-link]: https://matrix.to/#/#tridactyl:matrix.org */ /** ignore this line */ @@ -74,7 +74,7 @@ // Shared import * as Messaging from "@src/lib/messaging" -import { getTriVersion, browserBg, activeTab, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow, openInTab, queryAndURLwrangler } from "@src/lib/webext" +import { ownWinTriIndex, getTriVersion, browserBg, activeTab, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow, openInTab, queryAndURLwrangler } from "@src/lib/webext" import * as Container from "@src/lib/containers" import state from "@src/state" import { contentState, ModeName } from "@src/content/state_content" @@ -86,10 +86,14 @@ import { AutoContain } from "@src/lib/autocontainers" import * as CSS from "css" import * as Perf from "@src/perf" import * as Metadata from "@src/.metadata.generated" +import { ObjectType } from "../compiler/types/ObjectType" import * as Native from "@src/lib/native" import * as TTS from "@src/lib/text_to_speech" import * as excmd_parser from "@src/parsers/exmode" import * as escape from "@src/lib/escape" +import semverCompare from "semver-compare" +import * as hint_util from "@src/lib/hint_util" +import { OpenMode } from "@src/lib/hint_util" /** * This is used to drive some excmd handling in `composite`. @@ -129,7 +133,7 @@ import * as DOM from "@src/lib/dom" import * as CommandLineContent from "@src/content/commandline_content" import * as scrolling from "@src/content/scrolling" import { ownTab } from "@src/lib/webext" -import { wrap_input, getLineAndColNumber, rot13_helper } from "@src/lib/editor_utils" +import { rot13_helper, jumble_helper } from "@src/lib/editor_utils" import * as finding from "@src/content/finding" import * as toys from "./content/toys" import * as hinting from "@src/content/hinting" @@ -154,7 +158,6 @@ import "@src/lib/number.mod" import * as BGSELF from "@src/.excmds_background.generated" import { CmdlineCmds as BgCmdlineCmds } from "@src/background/commandline_cmds" import { EditorCmds as BgEditorCmds } from "@src/background/editor" -import { messageActiveTab } from "@src/lib/messaging" import { EditorCmds } from "@src/background/editor" import { firefoxVersionAtLeast } from "@src/lib/webext" import { parse_bind_args, modeMaps } from "@src/lib/binding" @@ -190,9 +193,9 @@ export async function getNativeVersion(): Promise { //#content export async function getRssLinks(): Promise> { const seen = new Set() - return Array.from(document.querySelectorAll("a, link[rel='alternate']")) - .filter((e: any) => typeof e.href === "string") - .reduce((acc, e: any) => { + return Array.from(document.querySelectorAll("a, link[rel='alternate']")) + .filter(e => typeof e.href === "string") + .reduce((acc, e) => { let type = "" // Start by detecting type because url doesn't necessarily contain the words "rss" or "atom" if (e.type) { @@ -201,8 +204,8 @@ export async function getRssLinks(): Promise { - ;[text, line, col] = getLineAndColNumber(t, start, end) - return [null, null, null] - })(elem) + const editor = getEditor(elem, { preferHTML: true }) + const text = await editor.getContent() + const pos = await editor.getCursor() + const file = (await Native.temp(text, document.location.hostname)).content - const exec = await Native.editor(file, line, col) + const exec = await Native.editor(file, ...pos) + if (exec.code == 0) { - fillinput(selector, exec.content) + await editor.setContent(exec.content) + // TODO: ask the editor nicely where its cursor was left and use that + // for now just try to put it where it started at + await editor.setCursor(...pos) // TODO: add annoying "This message was written with [Tridactyl](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/)" to everything written using editor - return [file, exec.content] + ans = [file, exec.content] } else { logger.debug(`Editor terminated with non-zero exit code: ${exec.code}`) } } catch (e) { throw new Error(`:editor failed: ${e}`) } finally { - return removeTridactylEditorClass(selector) + removeTridactylEditorClass(selector) + return ans } } @@ -381,7 +398,11 @@ export async function guiset_quiet(rule: string, option: string) { await Native.write(profile_dir + "/chrome/userChrome.css.tri.bak", cssstr) // Modify and write new CSS - const stylesheet = CSS.parse(cssstr) + const stylesheet = CSS.parse(cssstr, { silent: true }) + if (stylesheet.stylesheet.parsingErrors.length) { + const error = stylesheet.stylesheet.parsingErrors[0] + throw new Error(`Your current userChrome.css is malformed: ${error.reason} at ${error.line}:${error.column}. Fix or delete it and try again.`) + } // Trim due to https://github.com/reworkcss/css/issues/113 const stylesheetDone = CSS.stringify(css_util.changeCss(rule, option, stylesheet)).trim() return Native.write(profile_dir + "/chrome/userChrome.css", stylesheetDone) @@ -452,7 +473,10 @@ export async function loadtheme(themename: string) { // remove the "tridactylrc" bit so that we're left with the directory const path = (await Native.getrcpath()).split(separator).slice(0, -1).join(separator) + separator + "themes" + separator + themename + ".css" const file = await Native.read(path) - if (file.code !== 0) throw new Error("Couldn't read theme " + path) + if (file.code !== 0) { + if (Object.keys(await config.get("customthemes")).includes(themename)) return + throw new Error("Couldn't read theme " + path) + } return set("customthemes." + themename, file.content) } @@ -462,28 +486,53 @@ export async function unloadtheme(themename: string) { return unset("customthemes." + themename) } -/** Changes the current theme. +/** + * Changes the current theme. * * If THEMENAME is any of the themes that can be found in the [Tridactyl repo](https://github.com/tridactyl/tridactyl/tree/master/src/static/themes) (e.g. 'dark'), the theme will be loaded from Tridactyl's internal storage. * - * If THEMENAME is set to any other value, Tridactyl will attempt to use its native binary (see [[native]]) in order to load a CSS file named THEMENAME from disk. The CSS file has to be in a directory named "themes" and this directory has to be in the same directory as your tridactylrc. + * If THEMENAME is set to any other value except `--url`, Tridactyl will attempt to use its native binary (see [[native]]) in order to load a CSS file named THEMENAME from disk. The CSS file has to be in a directory named "themes" and this directory has to be in the same directory as your tridactylrc. If this fails, Tridactyl will attempt to load the theme from its internal storage. + * + * Lastly, themes can be loaded from URLs with `:colourscheme --url [url] [themename]`. They are stored internally - if you want to update the theme run the whole command again. * * Note that the theme name should NOT contain any dot. * * Example: `:colourscheme mysupertheme` * On linux, this will load ~/.config/tridactyl/themes/mysupertheme.css + * + * __NB__: due to Tridactyl's architecture, the theme will take a small amount of time to apply as each page is loaded. If this annoys you, you may use [userContent.css](http://kb.mozillazine.org/index.php?title=UserContent.css&printable=yes) to make changes to Tridactyl earlier. For example, users using the dark theme may like to put + * + * ``` + * :root { + * --tridactyl-bg: black !important; + * --tridactyl-fg: white !important; + * } + * ``` + * + * in their `userContent.css`. Follow [issue #2510](https://github.com/tridactyl/tridactyl/issues/2510) if you would like to find out when we have made a more user-friendly solution. */ //#background -export async function colourscheme(themename: string) { - // If this is a builtin theme, no need to bother with native messaging stuff +export async function colourscheme(...args: string[]) { + const themename = args[0] == "--url" ? args[2] : args[0] + + // If this is a builtin theme, no need to bother with slow stuff if (Metadata.staticThemes.includes(themename)) return set("theme", themename) if (themename.search("\\.") >= 0) throw new Error(`Theme name should not contain any dots! (given name: ${themename}).`) - await loadtheme(themename) + if (args[0] == "--url") { + if (themename === undefined) throw new Error(`You must provide a theme name!`) + let url = args[1] + if (url === "%") url = window.location.href // this is basically an easter egg + if (!(url.startsWith("http://") || url.startsWith("https://"))) url = "http://" + url + const css = await rc.fetchText(url) + set("customthemes." + themename, css) + } else { + await loadtheme(themename) + } return set("theme", themename) } /** - * Write a setting to your user.js file. + * Write a setting to your user.js file. Requires a [[restart]] after running to take effect. * * @param key The key that should be set. Must not be quoted. Must not contain spaces. * @param value The value the key should take. Quoted if a string, unquoted otherwise. @@ -668,25 +717,27 @@ export async function native() { if (version !== undefined) { done = fillcmdline("# Native messenger is correctly installed, version " + version) } else { - done = fillcmdline("# Native messenger not found. Please run `:installnative` and follow the instructions.") + done = fillcmdline("# Native messenger not found. Please run `:nativeinstall` and follow the instructions.") } return done } /** - * Simply copies "curl -fsSl https://raw.githubusercontent.com/tridactyl/tridactyl/master/native/install.sh | bash" to the clipboard and tells the user to run it. + * Copies the installation command for the native messenger to the clipboard and asks the user to run it in their shell. + * + * The native messenger's source code may be found here: https://github.com/tridactyl/native_messenger/blob/master/src/native_main.nim + * + * If your corporate IT policy disallows execution of binaries which have not been whitelisted but allows Python scripts, you may instead use the old native messenger by running `install.sh` or `win_install.ps1` from https://github.com/tridactyl/tridactyl/tree/master/native - the main downside is that it is significantly slower. */ //#background export async function nativeinstall() { const tag = TRI_VERSION.includes("pre") ? "master" : TRI_VERSION let done + const installstr = (await config.get("nativeinstallcmd")).replace("%TAG", tag) + await yank(installstr) if ((await browser.runtime.getPlatformInfo()).os === "win") { - const installstr = (await config.get("win_nativeinstallcmd")).replace("%WINTAG", "-Tag " + tag) - await yank(installstr) - done = fillcmdline("# Installation command copied to clipboard. Please paste and run it from cmd.exe, PowerShell, or MinTTY to install the native messenger.") + done = fillcmdline("# Installation command copied to clipboard. Please paste and run it in cmd.exe (other shells won't work) to install the native messenger.") } else { - const installstr = (await config.get("nativeinstallcmd")).replace("%TAG", tag) - await yank(installstr) done = fillcmdline("# Installation command copied to clipboard. Please paste and run it in your shell to install the native messenger.") } return done @@ -694,7 +745,7 @@ export async function nativeinstall() { /** Writes current config to a file. - NB: an RC file is not required for your settings to persist: all settings are stored in the Firefox Sync storage by default as soon as you set them. + NB: an RC file is not required for your settings to persist: all settings are stored in a local Firefox storage database by default as soon as you set them. With no arguments supplied the excmd will try to find an appropriate config path and write the rc file to there. Any argument given to the @@ -717,6 +768,8 @@ export async function nativeinstall() { Available flags: - `-f` will overwrite the config file if it exists. + - `--clipboard` write config to clipboard - no [[native]] required + @param args an optional string of arguments to be parsed. @returns the parsed config. @@ -737,6 +790,10 @@ export async function mktridactylrc(...args: string[]) { const file = argParse(args).join(" ") || undefined const conf = config.parseConfig() + if (file == "--clipboard") { + setclip(conf) + return fillcmdline_tmp(3000, "# RC copied to clipboard") + } if ((await Native.nativegate("0.1.11")) && !(await rc.writeRc(conf, overwrite, file))) logger.error("Could not write RC file") return conf @@ -755,7 +812,9 @@ export async function mktridactylrc(...args: string[]) { * * On Windows, the `~` expands to `%USERPROFILE%`. * - * The RC file is just a bunch of Tridactyl excmds (i.e, the stuff on this help page). Settings persist in local storage; add `sanitise tridactyllocal tridactylsync` to make it more Vim like. There's an [example file](https://raw.githubusercontent.com/tridactyl/tridactyl/master/.tridactylrc) if you want it. + * The RC file is just a bunch of Tridactyl excmds (i.e, the stuff on this help page). Settings persist in local storage. There's an [example file](https://raw.githubusercontent.com/tridactyl/tridactyl/master/.tridactylrc) if you want it. + * + * There is a [bug](https://github.com/tridactyl/tridactyl/issues/1409) where not all lines of the RC file are executed if you use `sanitise` at the top of it. We instead recommend you put `:bind ZZ composite sanitise tridactyllocal; qall` in your RC file and use `ZZ` to exit Firefox. * * @param args the file/URL to open. For files: must be an absolute path, but can contain environment variables and things like ~. */ @@ -764,7 +823,7 @@ export async function source(...args: string[]) { if (args[0] === "--url") { let url = args[1] if (!url || url === "%") url = window.location.href - if (!(url.startsWith("http://") || url.startsWith("https://"))) url = "http://" + url + if (!new RegExp("^(https?://)|data:").test(url)) url = "http://" + url await rc.sourceFromUrl(url) } else { const file = args.join(" ") || undefined @@ -793,20 +852,29 @@ export async function source_quiet(...args: string[]) { */ //#background export async function updatenative(interactive = true) { - const tag = TRI_VERSION.includes("pre") ? "master" : TRI_VERSION - if (await Native.nativegate("0", interactive)) { - if ((await browser.runtime.getPlatformInfo()).os === "mac") { - if (interactive) logger.error("Updating the native messenger on OSX is broken. Please use `:installnative` instead.") - return - } - if ((await browser.runtime.getPlatformInfo()).os === "win") { - await Native.run((await config.get("win_nativeinstallcmd")).replace("%WINTAG", "-Tag " + tag)) - } else { - await Native.run((await config.get("nativeinstallcmd")).replace("%TAG", tag)) - } - - if (interactive) native() + if (!(await Native.nativegate("0", interactive))) { + return + } else if ((await browser.runtime.getPlatformInfo()).os === "mac") { + if (interactive) logger.error("Updating the native messenger on OSX is broken. Please use `:nativeinstall` instead.") + return } + + const tag = TRI_VERSION.includes("pre") ? "master" : TRI_VERSION + const update_command = (await config.get("nativeinstallcmd")).replace("%TAG", tag) + const native_version = await Native.getNativeMessengerVersion() + + if (semverCompare(native_version, "0.2.0") < 0) { + await Native.run(update_command) + } else if (semverCompare(native_version, "0.3.1") < 0) { + if (interactive) { + throw new Error("Updating is broken on this version of the native messenger. Please use `:nativeinstall` instead.") + } + return + } else { + await Native.runAsync(update_command) + } + + if (interactive) native() } /** @@ -839,7 +907,7 @@ export async function restart() { /** Download the current document. * - * If you have the native messenger v>=0.1.9 installed, the function accepts one optional argument, filename, which can be: + * If you have the native messenger v>=0.1.9 installed, the function accepts an optional argument, filename, which can be: * - An absolute path * - A path starting with ~, which will be expanded to your home directory * - A relative path, relative to the native messenger executable (e.g. ~/.local/share/tridactyl on linux). @@ -847,12 +915,39 @@ export async function restart() { * * **NB**: if a non-default save location is chosen, Firefox's download manager will say the file is missing. It is not - it is where you asked it to be saved. * - * @param filename The name the file should be saved as. + * Flags: + * - `--overwrite`: overwrite the destination file. + * - `--cleanup`: removes the downloaded source file e.g. `$HOME/Downlods/downloaded.doc` if moving it to the desired directory fails. */ //#content -export async function saveas(...filename: string[]) { - if (filename.length > 0) { - return Messaging.message("download_background", "downloadUrlAs", window.location.href, filename.join(" ")) +export async function saveas(...args: string[]) { + let overwrite = false + let cleanup = false + + const argParse = (args: string[]): string[] => { + if (args[0] === "--overwrite") { + overwrite = true + args.shift() + argParse(args) + } + if (args[0] === "--cleanup") { + cleanup = true + args.shift() + argParse(args) + } + return args + } + + const file = argParse(args).join(" ") || undefined + + const requiredNativeMessengerVersion = "0.3.2" + if ((overwrite || cleanup) && !(await Native.nativegate(requiredNativeMessengerVersion, false))) { + throw new Error(`":saveas --{overwrite, cleanup}" requires native ${requiredNativeMessengerVersion} or later`) + } + + if (args.length > 0) { + const filename = await Messaging.message("download_background", "downloadUrlAs", window.location.href, file, overwrite, cleanup) + return fillcmdline_tmp(10000, `Download completed: ${filename} stored in ${file}`) } else { return Messaging.message("download_background", "downloadUrl", window.location.href, true) } @@ -968,7 +1063,7 @@ export function addJump() { // Prevent pending jump from being registered clearTimeout(JUMP_TIMEOUTID) // Schedule the registering of the current jump - const localTimeoutID = setTimeout(async () => { + const localTimeoutID = window.setTimeout(async () => { // Get config for current page const alljumps = await curJumps() // if this handler was cancelled after the call to curJumps(), bail out @@ -1078,12 +1173,14 @@ export function scrollline(n = 1, mult = 1) { } /** Scrolls the document by n pages. + * The height of a page is the current height of the window. * - * The height of a page is the current height of the window. + * @param count How many times to scroll. Used to facilitate key + * binds with counts for `` etc., not really useful otherwise. */ //#content -export function scrollpage(n = 1) { - return scrollpx(0, window.innerHeight * n) +export function scrollpage(n = 1, count = 1) { + return scrollpx(0, window.innerHeight * n * count) } /** @@ -1164,7 +1261,9 @@ export async function reloadall(hard = false) { return Promise.all(tabs.map(tab => browser.tabs.reload(tab.id, reloadprops))) } -/** Reloads all tabs except the current one, bypassing the cache if hard is set to true */ +/** Reloads all tabs except the current one, bypassing the cache if hard is set to true + * You probably want to use [[reloaddead]] instead if you just want to be able to ensure Tridactyl is loaded in all tabs where it can be + * */ //#background export async function reloadallbut(hard = false) { let tabs = await browser.tabs.query({ currentWindow: true }) @@ -1174,6 +1273,17 @@ export async function reloadallbut(hard = false) { return Promise.all(tabs.map(tab => browser.tabs.reload(tab.id, reloadprops))) } +//#background_helper +import { getTridactylTabs } from "@src/background/meta" +/** Reloads all tabs which Tridactyl isn't loaded in */ +//#background +export async function reloaddead(hard = false) { + const tabs = await browser.tabs.query({ currentWindow: true }) + const not_tridactyl_tabs = await getTridactylTabs(tabs, true) + const reloadprops = { bypassCache: hard } + return Promise.all(not_tridactyl_tabs.map(tab => browser.tabs.reload(tab.id, reloadprops))) +} + /** Reload the next n tabs, starting with activeTab. bypass cache for all */ //#background export async function reloadhard(n = 1) { @@ -1218,7 +1328,7 @@ export async function open(...urlarr: string[]) { `, ) } else { - const tab = await activeTab() + const tab = await ownTab() return openInTab(tab, {}, urlarr) } } @@ -1226,8 +1336,10 @@ export async function open(...urlarr: string[]) { /** * Works exactly like [[open]], but only suggests bookmarks. * + * If you want to use optional flags, you should run `:set completions.Bmark.autoselect false` to prevent the spacebar from inserting the URL of the top bookmark. + * * @param opt Optional. Has to be `-t` in order to make bmarks open your bookmarks in a new tab. - * @param urlarr any argument accepted by [[open]], or [[tabopen]] if opt is "-t" + * @param urlarr any argument accepted by [[open]], or [[tabopen]] if opt is "-t" (e.g. `-c [container]` to open a bookmark in a container) */ //#background export async function bmarks(opt: string, ...urlarr: string[]) { @@ -1288,7 +1400,7 @@ export async function url2args() { /** @hidden */ //#content_helper -let sourceElement +let sourceElement: Element /** @hidden */ //#content_helper function removeSource() { @@ -1476,7 +1588,7 @@ export async function apropos(...helpItems: string[]) { //#background export async function tutor(newtab?: string) { const tutor = browser.runtime.getURL("static/clippy/1-tutor.html") - let done + let done: Promise if (newtab) { done = tabopen(tutor) } else { @@ -1495,7 +1607,9 @@ export async function credits() { } /** - * Cover the current page in an overlay to prevent clicking on links with the mouse to force yourself to use hint mode. Get rid of it by reloading the page. + * Hides the cursor and covers the current page in an overlay to prevent clicking on links with the mouse to force yourself to use hint mode. + * + * To bring back mouse control, use [[mouse_mode]] or refresh the page. * * Suggested usage: `autocmd DocLoad .* no_mouse_mode` * @@ -1526,6 +1640,43 @@ export function snow_mouse_mode() { toys.snow() } +/** + * Music variant of [[no_mouse_mode]]. + */ +//#content +export function pied_piper_mouse_mode() { + toys.music() +} +/** + * Drawable variant of [[no_mouse_mode]] + * In this mode, you can use the mouse or a digital stylus to draw. To switch to an eraser, use [[drawingerasertoggle]] + * Use [[mouse_mode]] to return, or refresh page. + * Suggested usage: `autocmd DocLoad .* drawingstart + * + * **Warning**: Windows Ink enabled input devices don't work, disable it for your browser, or use a mouse. + */ +//#content +export function drawingstart() { + toys.drawable() +} +/** + * Switch between pen and eraser for [[drawingstart]] + * Suggested usage: `bind e drawingerasertoggle`. If you have a digital pen, map the button to `e` to switch easily. + */ +//#content +export function drawingerasertoggle() { + toys.eraser_toggle() +} +/** + * Revert any variant of the [[no_mouse_mode]] + * + * Suggested usage: `bind mouse_mode` with the autocmd mentioned in [[no_mouse_mode]] or [[drawingstart]]. + */ +//#content +export function mouse_mode() { + toys.removeBlock() +} + /** @hidden */ // Find clickable next-page/previous-page links whose text matches the supplied pattern, // and return the last such link. @@ -1595,10 +1746,12 @@ export function followpage(rel: "next" | "prev" = "next") { /** Increment the current tab URL * * @param count the increment step, can be positive or negative + * + * @param multiplier multiplies the count so that e.g. `5` works. */ //#content -export function urlincrement(count = 1) { - const newUrl = UrlUtil.incrementUrl(window.location.href, count) +export function urlincrement(count = 1, multiplier = 1) { + const newUrl = UrlUtil.incrementUrl(window.location.href, count * multiplier) if (newUrl !== null) { // This might throw an error when using incrementurl on a moz-extension:// page if the page we're trying to access doesn't exist @@ -1694,6 +1847,18 @@ export function urlparent(count = 1) { * * `http://e.com/issues/42` -> (`-g -1 foo`) -> `http://e.com/issues/42/foo` * * `http://e.com/issues/42` -> (`-g -2 foo`) -> `http://e.com/issues/foo` * + * + * * URL Input: `urlmodify -*u ` + * + * Each mode can be augmented to accept a URL as the last argument instead of + * the current url. + * + * Examples: + * + * * `urlmodify -tu ` + * * `urlmodify -su ` + * * `urlmodify -gu ` + * * @param mode The replace mode: * * -t text replace * * -r regexp replace @@ -1701,6 +1866,7 @@ export function urlparent(count = 1) { * * -q replace the value of the given query * * -Q delete the given query * * -g graft a new path onto URL or parent path of it + * * -*u Use last argument as URL input instead of current URL * @param replacement the replacement arguments (depends on mode): * * -t * * -r [flags] @@ -1708,9 +1874,10 @@ export function urlparent(count = 1) { * * -q * * -Q * * -g + * * -*u */ //#content -export function urlmodify(mode: "-t" | "-r" | "-s" | "-q" | "-Q" | "-g", ...args: string[]) { +export function urlmodify(mode: "-t" | "-r" | "-s" | "-q" | "-Q" | "-g" | "-tu" | "-ru" | "-su" | "-qu" | "-Qu" | "-gu", ...args: string[]) { const newUrl = urlmodify_js(mode, ...args) // TODO: once we have an arg parser, have a quiet flag that prevents the page from being added to history if (newUrl && newUrl !== window.location.href) { @@ -1726,11 +1893,19 @@ export function urlmodify(mode: "-t" | "-r" | "-s" | "-q" | "-Q" | "-g", ...args * `:composite urlmodify_js -t www. old. | tabopen ` */ //#content -export function urlmodify_js(mode: "-t" | "-r" | "-s" | "-q" | "-Q" | "-g", ...args: string[]) { - const oldUrl = new URL(window.location.href) +export function urlmodify_js(mode: "-t" | "-r" | "-s" | "-q" | "-Q" | "-g" | "-tu" | "-ru" | "-su" | "-qu" | "-Qu" | "-gu", ...args: string[]) { + let oldUrl + let newmode + if (mode.slice(-1) == "u") { + oldUrl = new URL(args.pop()) + newmode = mode.slice(0, -1) + } else { + oldUrl = new URL(window.location.href) + newmode = mode + } let newUrl - switch (mode) { + switch (newmode) { case "-t": if (args.length !== 2) { throw new Error("Text replacement needs 2 arguments:" + " ") @@ -1801,16 +1976,17 @@ export async function geturlsforlinks(reltype = "rel", rel: string) { /** Sets the current page's zoom level anywhere between 30% and 300%. * - * If you overshoot the level while using relative adjustments i.e. level > 300% or level < 30% - * the zoom level will be set to it's maximum or minimum position. + * If you overshoot the level while using relative adjustments i.e. level > 300% or level < 30% the zoom level will be set to it's maximum or minimum position. Relative adjustments are made * in percentage points, i.e. `:zoom +10 true` increases the zoom level from 50% to 60% or from * 200% to 210%. * - * @param level - The zoom level to set. - * Expects percentages when changing the absolute zoom value and percentage points when making relative adjustments. + * @param level - The zoom level to set. Treated as percentage value if larger than 3 or smaller than -3. * @param rel - Set the zoom adjustment to be relative to current zoom level. + * @param tabId - The tabId to apply zoom level too. + * If set to 'auto' it will default to the current active tab. + * This uses mozilla's internal tabId and not tridactyl's tabId. */ //#background -export async function zoom(level = 0, rel = "false") { - level = level > 3 ? level / 100 : level +export async function zoom(level = 0, rel = "false", tabId = "auto") { + level = Math.abs(level) > 3 ? level / 100 : level if (rel === "false" && (level > 3 || level < 0.3)) { throw new Error(`[zoom] level out of range: ${level}`) } @@ -1821,7 +1997,12 @@ export async function zoom(level = 0, rel = "false") { if (level > 3) level = 3 if (level < 0.3) level = 0.3 } - return browser.tabs.setZoom(level) + + if (tabId === "auto") { + return browser.tabs.setZoom(level) + } else { + return browser.tabs.setZoom(parseInt(tabId, 10), level) + } } /** Opens the current page in Firefox's reader mode. @@ -1843,8 +2024,26 @@ export async function reader() { //#content_helper // { loadaucmds("DocStart") +const autocmd_logger = new Logging.Logger("autocmds") window.addEventListener("pagehide", () => loadaucmds("DocEnd")) window.addEventListener("DOMContentLoaded", () => loadaucmds("DocLoad")) + +// Unsupported edge-case: a SPA that doesn't have a UriChange autocmd changes URL to one that does. +config.getAsync("autocmds", "UriChange").then(ausites => { + if (!ausites) return + const aukeyarr = Object.keys(ausites).filter(e => window.document.location.href.search(e) >= 0) + if (aukeyarr.length > 0) { + let currUri = window.document.location.href + function maybeLoad() { + const nowUri = window.document.location.href + if (nowUri != currUri) { + currUri = nowUri + loadaucmds("UriChange") + } + } + setInterval(maybeLoad, 100) + } +}) /** @hidden */ const fullscreenhandler = () => { loadaucmds("FullscreenChange") @@ -1868,15 +2067,40 @@ if (fullscreenApiIsPrefixed) { /** @hidden */ //#content -export async function loadaucmds(cmdType: "DocStart" | "DocLoad" | "DocEnd" | "TabEnter" | "TabLeft" | "FullscreenEnter" | "FullscreenLeft" | "FullscreenChange") { +export async function loadaucmds(cmdType: "DocStart" | "DocLoad" | "DocEnd" | "TabEnter" | "TabLeft" | "FullscreenEnter" | "FullscreenLeft" | "FullscreenChange" | "UriChange") { const aucmds = await config.getAsync("autocmds", cmdType) const ausites = Object.keys(aucmds) const aukeyarr = ausites.filter(e => window.document.location.href.search(e) >= 0) + const owntab = await ownTab() + const replacements = { + TRI_FIRED_MOZ_TABID: owntab.id, + TRI_FIRED_TRI_TABINDEX: owntab.index + 1, + TRI_FIRED_MOZ_WINID: owntab.windowId, + TRI_FIRED_TRI_WININDEX: await ownWinTriIndex(), + TRI_FIRED_MOZ_OPENERTABID: owntab.openerTabId, + TRI_FIRED_ACTIVE: owntab.active, + TRI_FIRED_AUDIBLE: owntab.audible, + TRI_FIRED_MUTED: owntab.mutedInfo.muted, + TRI_FIRED_DISCARDED: owntab.discarded, + TRI_FIRED_HEIGHT: owntab.height, + TRI_FIRED_WIDTH: owntab.width, + TRI_FIRED_HIDDEN: owntab.hidden, + TRI_FIRED_INCOGNITO: owntab.incognito, + TRI_FIRED_ISARTICLE: owntab.isArticle, + TRI_FIRED_LASTACCESSED: owntab.lastAccessed, + TRI_FIRED_PINNED: owntab.pinned, + TRI_FIRED_TITLE: owntab.title, + TRI_FIRED_URL: owntab.url, + } for (const aukey of aukeyarr) { + for (const [k, v] of Object.entries(replacements)) { + aucmds[aukey] = aucmds[aukey].replace(k, v) + } try { + autocmd_logger.debug(`${cmdType} matched ${aukey}: ${aucmds[aukey]}`) await controller.acceptExCmd(aucmds[aukey]) } catch (e) { - logger.error(e.toString()) + autocmd_logger.error((e as Error).toString()) } } } @@ -1906,7 +2130,8 @@ input:not([disabled]):not([readonly]):-moz-any( ), textarea:not([disabled]):not([readonly]), object, -[role='application'] +[role='application'], +[contenteditable='true'][role='textbox'] ` /** Password field selectors @@ -1999,7 +2224,7 @@ export function focusinput(nth: number | string) { * Currently just goes to the last focussed input; being able to jump forwards and backwards is planned. */ //#background -export async function changelistjump(n?: number) { +export async function changelistjump() { const tail = state.prevInputs[state.prevInputs.length - 1] const jumppos = tail.jumppos ? tail.jumppos : state.prevInputs.length - 1 const input = state.prevInputs[jumppos] @@ -2065,7 +2290,7 @@ export async function tabnext(increment = 1) { */ //#background export async function tabnext_gt(index?: number) { - let done + let done: Promise if (index === undefined) { done = tabnext() } else { @@ -2080,9 +2305,6 @@ export async function tabnext_gt(index?: number) { */ //#background export async function tabprev(increment = 1) { - // Proper way: - // return tabIndexSetActive((await activeTab()).index - increment + 1) - // Kludge until https://bugzilla.mozilla.org/show_bug.cgi?id=1504775 is fixed: return browser.tabs.query({ currentWindow: true, hidden: false }).then(tabs => { tabs.sort((t1, t2) => t1.index - t2.index) const prevTab = (tabs.findIndex(t => t.active) - increment + tabs.length) % tabs.length @@ -2094,10 +2316,27 @@ export async function tabprev(increment = 1) { * Pushes the current tab to another window. Only works for windows of the same type * (can't push a non-private tab to a private window or a private tab to * a non-private window). + * If *windowId* is not specified, pushes to the next newest window, + * wrapping around. */ //#background -export async function tabpush(windowId: number) { - return activeTabId().then(tabId => browser.tabs.move(tabId, { index: -1, windowId })) +export async function tabpush(windowId?: number) { + const currentWindow = await browser.windows.getCurrent() + const windows = (await browser.windows.getAll()).filter(w => w.incognito === currentWindow.incognito) + windows.sort((w1, w2) => w1.id - w2.id) + const nextWindow = windows[(windows.findIndex(window => window.id === currentWindow.id) + 1) % windows.length] + const tabId = await activeTabId() + return browser.tabs.move(tabId, { index: -1, windowId: windowId ?? nextWindow.id }) +} + +/** Switch to the tab currently playing audio, if any. */ +//#background +export async function tabaudio() { + const tabs = await browser.tabs.query({ audible: true }) + if (tabs.length > 0) { + await browser.windows.update(tabs[0].windowId, { focused: true }) + return browser.tabs.update(tabs[0].id, { active: true }) + } } /** @@ -2153,8 +2392,12 @@ export async function tabgrab(id: string) { /** Like [[open]], but in a new tab. If no address is given, it will open the newtab page, which can be set with `set newtab [url]` Use the `-c` flag followed by a container name to open a tab in said container. Tridactyl will try to fuzzy match a name if an exact match is not found (opening the tab in no container can be enforced with "firefox-default" or "none"). If any autocontainer directives are configured and -c is not set, Tridactyl will try to use the right container automatically using your configurations. + Use the `-b` flag to open the tab in the background. - These two can be combined in any order, but need to be placed as the first arguments. + + Use the `-w` flag to wait for the web page to load before "returning". This only makes sense for use with [[composite]], which waits for each command to return before moving on to the next one, e.g. `composite tabopen -b -w news.bbc.co.uk ; tabnext`. + + These three can be combined in any order, but need to be placed as the first arguments. Unlike Firefox's Ctrl-t shortcut, this opens tabs immediately after the currently active tab rather than at the end of the tab list because that is @@ -2173,17 +2416,23 @@ export async function tabgrab(id: string) { //#background export async function tabopen(...addressarr: string[]): Promise { let active + let waitForDom let container const win = await browser.windows.getCurrent() // Lets us pass both -b and -c in no particular order as long as they are up front. - async function argParse(args): Promise { + async function argParse(args: string[]): Promise { if (args[0] === "-b") { active = false args.shift() argParse(args) + } else if (args[0] === "-w") { + waitForDom = true + args.shift() + argParse(args) } else if (args[0] === "-c") { + if (args.length < 2) throw new Error(`You must provide a container name!`) // Ignore the -c flag if incognito as containers are disabled. if (!win.incognito) { if (args[1] === "firefox-default" || args[1].toLowerCase() === "none") { @@ -2204,7 +2453,7 @@ export async function tabopen(...addressarr: string[]): Promise80. `:set searchengine google` or see issue https://github.com/tridactyl/tridactyl/issues/2695") } // This ignores :set tabopenpos / issue #342. TODO: fix that somehow. return browser.search.search(maybeURL) } - return openInNewTab(null, args).then(tab => browser.search.search({ tabId: tab.id, ...maybeURL })) + return openInNewTab(null, args, waitForDom).then(tab => browser.search.search({ tabId: tab.id, ...maybeURL })) } // Fall back to about:newtab - return openInNewTab(null, args) + return openInNewTab(null, args, waitForDom) } /** @@ -2270,7 +2519,7 @@ export function tabqueue(...addresses: string[]) { } return tabopen("-b", addresses[0]).then( tab => - new Promise((resolve, reject) => { + new Promise(resolve => { function openNextTab(activeInfo) { if (activeInfo.tabId === tab.id) { resolve(tabqueue(...addresses.slice(1))) @@ -2309,6 +2558,16 @@ export function tabqueue(...addresses: string[]) { */ //#background_helper async function idFromIndex(index?: number | "%" | "#" | string): Promise { + return (await tabFromIndex(index)).id +} + +/** + * Like [[idFromIndex]] but returns the whole tab object + * + * @hidden + */ +//#background_helper +async function tabFromIndex(index?: number | "%" | "#" | string): Promise { if (index === "#") { // Support magic previous/current tab syntax everywhere const tabs = await getSortedWinTabs() @@ -2316,9 +2575,9 @@ async function idFromIndex(index?: number | "%" | "#" | string): Promise // In vim, '#' is the id of the previous buffer even if said buffer has been wiped // However, firefox doesn't store tab ids for closed tabs // Since vim makes '#' default to the current buffer if only one buffer has ever been opened for the current session, it seems reasonable to return the id of the current tab if only one tab is opened in firefox - return activeTabId() + return activeTab() } - return tabs[1].id + return tabs[1] } else if (index !== undefined && index !== "%") { // Wrap index = Number(index) @@ -2330,9 +2589,9 @@ async function idFromIndex(index?: number | "%" | "#" | string): Promise currentWindow: true, index: index - 1, }) - )[0].id + )[0] } else { - return activeTabId() + return activeTab() } } @@ -2401,15 +2660,24 @@ export async function fullscreen() { */ //#background export async function tabclose(...indexes: string[]) { - let done - if (indexes.length > 0) { - const ids = await Promise.all(indexes.map(index => idFromIndex(index))) - done = browser.tabs.remove(ids) - } else { - // Close current tab - done = browser.tabs.remove(await activeTabId()) + async function maybeWinTabToTab(id: string) { + if (id.includes(".")) { + const [winid, tabindex_number] = await parseWinTabIndex(id) + return (await browser.tabs.query({ windowId: winid, index: tabindex_number }))[0] + } + return tabFromIndex(id) } - return done + const tabs = await Promise.all(indexes.length > 0 ? indexes.map(maybeWinTabToTab) : [activeTab()]) + const tabclosepinned = (await config.getAsync("tabclosepinned")) === "true" + if (!tabclosepinned) { + // Pinned tabs should not be closed, abort if one of the tabs is pinned + for (const tab of tabs) { + if (tab.pinned) { + throw new Error(`Tab ${tab.windowId}:${tab.index + 1} is pinned and tabclosepinned is false, aborting tabclose`) + } + } + } + return browser.tabs.remove(tabs.map(t => t.id)) } /** Close all tabs to the side specified @@ -2436,10 +2704,11 @@ export async function tabcloseallto(side: string) { current window unless the most recently closed item is a window. Supplying either "tab" or "window" as an argument will specifically only - restore an item of the specified type. + restore an item of the specified type. Supplying "tab_strict" only restores + tabs that were open in the current window. @param item - The type of item to restore. Valid inputs are "recent", "tab" and "window". + The type of item to restore. Valid inputs are "recent", "tab", "tab_strict" and "window". @return The tab or window id of the restored item. Returns -1 if no items are found. */ @@ -2448,52 +2717,27 @@ export async function undo(item = "recent"): Promise { const current_win_id: number = (await browser.windows.getCurrent()).id const sessions = await browser.sessions.getRecentlyClosed() - if (item === "tab") { - const lastSession = sessions.find(s => { - if (s.tab) return true - }) - if (lastSession) { - browser.sessions.restore(lastSession.tab.sessionId) - return lastSession.tab.id - } - } else if (item === "window") { - const lastSession = sessions.find(s => { - if (s.window) return true - }) - if (lastSession) { - browser.sessions.restore(lastSession.window.sessionId) - return lastSession.window.id - } - } else if (item === "recent") { - // The first session object that's a window or a tab from this window. Or undefined if sessions is empty. - const lastSession = sessions.find(s => { - if (s.window) { - return true - } else if (s.tab && s.tab.windowId === current_win_id) { - return true - } else { - return false - } - }) + // Pick the first session object that is a window or a tab from this window ("recent"), a tab ("tab"), a tab + // from this window ("tab_strict"), a window ("window") or pick by sessionId. + const predicate = + item === "recent" + ? s => s.window || (s.tab && s.tab.windowId === current_win_id) + : item === "tab" + ? s => s.tab + : item === "tab_strict" + ? s => s.tab && s.tab.windowId === current_win_id + : item === "window" + ? s => s.window + : !isNaN(parseInt(item, 10)) + ? s => (s.tab || s.window).sessionId === item + : () => { + throw new Error(`[undo] Invalid argument: ${item}. Must be one of "recent, "tab", "tab_strict", "window" or a sessionId (by selecting a session using the undo completion).`) + } // this won't throw an error if there isn't anything in the session list, but I don't think that matters + const session = sessions.find(predicate) - if (lastSession) { - if (lastSession.tab) { - browser.sessions.restore(lastSession.tab.sessionId) - return lastSession.tab.id - } else if (lastSession.window) { - browser.sessions.restore(lastSession.window.sessionId) - return lastSession.window.id - } - } - } else if (!isNaN(parseInt(item, 10))) { - const sessionId = item - const session = sessions.find(s => (s.tab || s.window).sessionId === sessionId) - if (session) { - browser.sessions.restore(sessionId) - return (session.tab || session.window).id - } - } else { - throw new Error(`[undo] Invalid argument: ${item}. Must be one of "tab", "window", "recent"`) + if (session) { + browser.sessions.restore((session.tab || session.window).sessionId) + return (session.tab || session.window).id } return -1 } @@ -2562,6 +2806,26 @@ export async function tabmove(index = "$") { return browser.tabs.move(aTab.id, { index: newindex }) } +/** + * Move tabs in current window according to various criteria: + * + * - `--containers` groups tabs by containers + * - `--title` sorts tabs by title + * - `--url` sorts tabs by url (the default) + * - `(tab1, tab2) => true|false` + * - sort by arbitrary comparison function. `tab{1,2}` are objects with properties described here: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab + */ +//#background +export async function tabsort(...callbackchunks: string[]) { + const argument = callbackchunks.join(" ") + const comparator = argument == "--containers" ? (l, r) => l.cookieStoreId < r.cookieStoreId : argument == "--title" ? (l, r) => l.title < r.title : argument == "--url" || argument == "" ? (l, r) => l.url < r.url : eval(argument) + const windowTabs = await browser.tabs.query({ currentWindow: true }) + windowTabs.sort(comparator) + Object.entries(windowTabs).forEach(([index, tab]) => { + browser.tabs.move(tab.id, { index: parseInt(index, 10) }) + }) +} + /** Pin the current tab */ //#background export async function pin() { @@ -2609,7 +2873,7 @@ export async function mute(...muteArgs: string[]): Promise { if (mute) { updateObj.muted = true } - let done + let done: Promise if (all) { const tabs = await browser.tabs.query({ currentWindow: true }) const promises = [] @@ -2639,24 +2903,33 @@ export async function mute(...muteArgs: string[]): Promise { * * `winopen -popup [...]` will open it in a popup window. You can combine the two for a private popup. * + * `winopen -c containername [...]` will open the result in a container while ignoring other options given. See [[tabopen]] for more details on containers. + * * Example: `winopen -popup -private ddg.gg` */ //#background export async function winopen(...args: string[]) { - const createData = {} as any + const createData = {} as Parameters[0] let firefoxArgs = "--new-window" let done = false + let useContainer = false while (!done) { switch (args[0]) { case "-private": createData.incognito = true - args = args.slice(1, args.length) + args.shift() firefoxArgs = "--private-window" break case "-popup": createData.type = "popup" - args = args.slice(1, args.length) + args.shift() + break + + case "-c": + if (args.length < 2) throw new Error(`You must provide a container name!`) + args.shift() + useContainer = true break default: @@ -2666,10 +2939,22 @@ export async function winopen(...args: string[]) { } const address = args.join(" ") + + if (useContainer) { + if (firefoxArgs === "--private-window") { + throw new Error("Can't open a container in a private browsing window.") + } else { + args.unshift("-c") + return tabopen(...args).then(() => tabdetach()) + } + } + if (!ABOUT_WHITELIST.includes(address) && /^(about|file):.*/.exec(address)) { return nativeopen(firefoxArgs, address) } + createData.url = "https://fix-a-firefox-bug.invalid" + return browser.windows.create(createData).then(win => openInTab(win.tabs[0], { loadReplace: true }, address.split(" "))) } @@ -2768,7 +3053,7 @@ export async function viewcontainers() { // # and white space don't agree with FF's JSON viewer. // Probably other symbols too. const containers = await browserBg.contextualIdentities.query({}) // Can't access src/lib/containers.ts from a content script. - window.location.href = "data:application/json," + JSON.stringify(containers).replace(/#/g, "%23").replace(/ /g, "%20") + jsonview(JSON.stringify(containers)) } /** Opens the current tab in another container. @@ -3159,7 +3444,7 @@ export async function shellescape(...quoteme: string[]) { } //#background_helper -import * as useractions from "@src/background/user_actions" +import { useractions } from "@src/background/user_actions" /** * Magic escape hatch: if Tridactyl can't run in the current tab, return to a tab in the current window where Tridactyl can run, making such a tab if it doesn't currently exist. If Tridactyl can run in the current tab, return focus to the document body from e.g. the URL bar or a video player. @@ -3168,6 +3453,8 @@ import * as useractions from "@src/background/user_actions" * * NB: when called via `bind --mode=browser`, we return focus from the address bar by opening and closing the "sidebar" (which is used exclusively for this purpose). If escapehatch is called in any other way, we cannot do this as Mozilla thinks it might [spook](https://extensionworkshop.com/documentation/publish/add-on-policies/#no-surprises) [you](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/User_actions) : ). * + * This sidebar hack will close other sidebars such a TreestyleTabs. You can disable it with `:set escapehatchsidebarhack false`, but Tridactyl will no longer be able to get focus back from certain places such as the address bar. + * */ //#background export async function escapehatch() { @@ -3230,7 +3517,7 @@ export async function fillcmdline_tmp(ms: number, ...strarr: string[]) { const str = strarr.join(" ") showcmdline(false) Messaging.messageOwnTab("commandline_frame", "fillcmdline", [strarr.join(" "), false, false]) - return new Promise(resolve => + return new Promise(resolve => setTimeout(async () => { if ((await Messaging.messageOwnTab("commandline_frame", "getContent", [])) === str) { CommandLineContent.hide_and_blur() @@ -3252,48 +3539,65 @@ export function yank(...content: string[]) { } /** - * Copies a string to the clipboard/selection buffer depending on the user's preferences + * Copies a string to the clipboard/selection buffer depending on the user's preferences. * * @hidden */ //#background_helper -async function setclip(str) { - // Functions to avoid retyping everything everywhere +async function setclip(data: string) { + // Function to avoid retyping everything everywhere + const setclip_selection = data => Native.clipboard("set", data) - // Note: We're using fillcmdline here because exceptions are somehow not caught. We're rethrowing because otherwise the error message will be overwritten with the "yank successful" message. - const s = () => Native.clipboard("set", str) - const c = () => messageActiveTab("commandline_frame", "setClipboard", [str]) - - let promises = [] + let promises: Promise[] switch (await config.getAsync("yankto")) { case "selection": - promises = [s()] + promises = [setclip_selection(data)] break case "clipboard": - promises = [c()] + promises = [setclip_webapi(data)] break case "both": - promises = [s(), c()] + promises = [setclip_selection(data), setclip_webapi(data)] break } return Promise.all(promises) } +/** + * Copies a string to the clipboard using the Clipboard API. + * @hidden + * + * Has to be a background helper as it's only available on HTTPS and background pages. We want to be able to copy stuff to the clipboard from HTTP pages too. + */ +//#background_helper +async function setclip_webapi(data: string) { + return window.navigator.clipboard.writeText(data) +} + /** * Fetches the content of the clipboard/selection buffer depending on user's preferences * * Exposed for use with [[composite]], e.g. `composite getclip | fillcmdline` */ //#background -export async function getclip(fromm?: "clipboard" | "selection") { - if (fromm === undefined) fromm = await config.getAsync("putfrom") - if (fromm === "clipboard") { - return messageActiveTab("commandline_frame", "getClipboard") +export async function getclip(from?: "clipboard" | "selection") { + if (from === undefined) from = await config.getAsync("putfrom") + if (from === "clipboard") { + return getclip_webapi() } else { return Native.clipboard("get", "") } } +/** + * Gets the clipboard content using the Clipboard API. + * @hidden + */ +//#background_helper +async function getclip_webapi() { + return window.navigator.clipboard.readText() +} + /** Use the system clipboard. If `excmd === "open"`, call [[open]] with the contents of the clipboard. Similarly for [[tabopen]]. @@ -3306,7 +3610,7 @@ export async function getclip(fromm?: "clipboard" | "selection") { If `excmd === "yanktitle"`, copy the title of the open page. - If `excmd === "yankmd"`, copy the title and url of the open page formatted in Markdown for easy use on sites such as reddit. + If `excmd === "yankmd"`, copy the title and url of the open page formatted in Markdown for easy use on sites such as reddit. `yankorg` is similar but for Emacs orgmode. If you're on Linux and the native messenger is installed, Tridactyl will call an external binary (either xclip or xsel) to read or write to your X selection buffer. If you want another program to be used, set "externalclipboardcmd" to its name and make sure it has the same interface as xsel/xclip ("-i"/"-o" and reading from stdin). @@ -3316,7 +3620,7 @@ export async function getclip(fromm?: "clipboard" | "selection") { */ //#background -export async function clipboard(excmd: "open" | "yank" | "yankshort" | "yankcanon" | "yanktitle" | "yankmd" | "xselpaste" | "tabopen" = "open", ...toYank: string[]) { +export async function clipboard(excmd: "open" | "yank" | "yankshort" | "yankcanon" | "yanktitle" | "yankmd" | "yankorg" | "xselpaste" | "tabopen" = "open", ...toYank: string[]) { let content = toYank.join(" ") let url = "" let urls = [] @@ -3356,6 +3660,11 @@ export async function clipboard(excmd: "open" | "yank" | "yankshort" | "yankcano await yank(content) done = fillcmdline_tmp(3000, "# " + content + " copied to clipboard.") break + case "yankorg": + content = "[[" + (await activeTab()).url + "][" + (await activeTab()).title + "]]" + await yank(content) + done = fillcmdline_tmp(3000, "# " + content + " copied to clipboard.") + break case "open": url = await getclip() if (url) { @@ -3381,28 +3690,114 @@ export async function clipboard(excmd: "open" | "yank" | "yankshort" | "yankcano return done.then(() => undefined) } -/** Change active tab. +/** Copy an image to the clipboard. - @param index - Starts at 1. 0 refers to last tab of the current window, -1 to penultimate tab, etc. - - "#" means the tab that was last accessed in this window - - This is different from [[taball]] because `index` is the position of the tab in the current window. - */ + @param url + Absolute URL to the image to be copied. You can obtain an absolute URL from a relative one using [tri.urlutils.getAbsoluteURL](_src_lib_url_util_.html#getabsoluteurl). +*/ //#background -export async function tab(index: number | "#") { - return tabIndexSetActive(index) +export async function yankimage(url: string): Promise { + const absoluteUrl = UrlUtil.getAbsoluteURL(url, document.baseURI) + const image = await window.fetch(absoluteUrl) + const blob = await image.blob() + // Blob.type returns a MIME type like "image/jpeg; charset=UTF-8", but the Clipboard API needs a type like "jpeg" + const imageType = blob.type.split("/")[1].split(";")[0] + try { + browser.clipboard.setImageData(await blob.arrayBuffer(), imageType as browser.clipboard._SetImageDataImageType) + } catch (err) { + if (err instanceof Error && err.message.includes("imageType")) { + throw new Error(`Image type ${blob.type} is not supported`) + } else { + throw err + } + } } /** Change active tab. @param id - A string following the following format: "[0-9]+.[0-9]+", the first number being the index of the window that should be selected and the second one being the index of the tab within that window. + A bare number means the current window is used. Starts at 1. 0 refers to last tab of the current window, -1 to penultimate tab, etc. + A string following the following format: "[0-9]+.[0-9]+" means the first number being the index of the window that should be selected and the second one being the index of the tab within that window. [[taball]] has completions for this format. + + "#" means the tab that was last accessed in this window + + A non integer string means to search the URL and title for matches, in this window if called from tab, all windows if called from anytab. Title matches can contain '*' as a wildcard. */ //#background -export async function taball(id: string) { +export async function tab(...id: string[]) { + return tab_helper(true, false, ...id) +} + +/** Wrapper for [[tab]] with multi-window completions + */ +//#background +export async function taball(...id: string[]) { + return tab_helper(true, true, ...id) +} + +/** Rename current tab. + @hidden + + @param name + Tab name. +*/ +//#content_helper +export function tabcurrentrename(...name: string[]) { + document.title = name.join(" ") +} + +/** Rename a tab. + + @param index + Index of the target tab. + + @param name + Tab name. +*/ +//#background +export async function tabrename(index: string, ...name: string[]) { + const id = await idFromIndex(index) + return Messaging.messageTab(id, "excmd_content", "tabcurrentrename", name) +} + +/** Helper to change active tab. Used by [[tab]] and [[taball]]. + + @param interactive + Controls if we should prompt if multiple matches are found, or just pick the first match + + @param anyWindow + True if we should search in all windows, or just the current one. + + @param key + String or int tab search key, see [[tab]] for usage. + */ +//#background +export async function tab_helper(interactive: boolean, anyWindow: boolean, ...key: string[]) { + const id = key.join(" ") + if (Number.isInteger(Number(id))) return tabIndexSetActive(Number(id)) + if (id === "#") return tabIndexSetActive(id) + + if (id !== null && id !== undefined && !/\d+\.\d+/.exec(id)) { + let defaultQuery = {} + if (!anyWindow) defaultQuery = { windowId: (await activeTab()).windowId } + + const results = new Map() + try { + ;(await browser.tabs.query({ ...defaultQuery, ...{ url: id } })).forEach(tab => results.set(tab.id, tab)) + } catch (e) {} + if (results.size < 2) (await browser.tabs.query({ ...defaultQuery, ...{ title: id.replace("*", "\\*") } })).forEach(tab => results.set(tab.id, tab)) + if (results.size < 2) (await browser.tabs.query(defaultQuery)).filter(tab => tab.url.includes(id)).forEach(tab => results.set(tab.id, tab)) + if (results.size < 2) (await browser.tabs.query({ ...defaultQuery, ...{ title: "*" + id + "*" } })).forEach(tab => results.set(tab.id, tab)) + if (results.size) { + if (interactive && results.size > 1) return fillcmdline_notrail(anyWindow ? "taball" : "tab", id) + const firstTab = results.values().next().value + await browser.windows.update(firstTab.windowId, { focused: true }) + return browser.tabs.update(firstTab.id, { active: true }) + } + throw new Error("No tab found matching: " + id) + } + const [winid, tabindex_number] = await parseWinTabIndex(id) const tabid = (await browser.tabs.query({ windowId: winid, index: tabindex_number }))[0].id await browser.windows.update(winid, { focused: true }) @@ -3427,9 +3822,10 @@ export async function taball(id: string) { * - `command hello t` This will expand recursively into 'hello'->'tabopen' * * Commands/aliases are expanded as in a shell, so, given the commands above, - * entering `:tn 43` will expand to `:tabnext_gt 43`. + * entering `:tn 43` will expand to `:tabnext_gt 43`. You can use this to create + * your own ex-commands in conjunction with [[js]], specifically `js -p` and `js -d`. * - * Note that this is only for excmd->excmd mappings. To map a normal-mode + * Note that this is only for excmd -> excmd mappings. To map a normal-mode * command to an excommand, see [[bind]]. * * See also: @@ -3563,7 +3959,7 @@ export function bindurl(pattern: string, mode: string, keys: string, ...excmd: s } /** - * Makes one key equivalent to another for the purposes of most of our parsers. Useful for international keyboard layouts. + * Makes one key equivalent to another for the purposes of most of our parsers. Useful for international keyboard layouts. See user-provided examples for various layouts on our wiki: https://github.com/tridactyl/tridactyl/wiki/Internationalisation * * e.g, * keymap ę e @@ -3598,7 +3994,7 @@ function validateSetArgs(key: string, values: string[]) { const strval = values.join(" ") // Note: the conversion will throw if strval can't be converted to the right type if (md.type.kind === "object" && target.length > 1) { - value = (md as any).type.convertMember(target.slice(1), strval) + value = (md.type as ObjectType).convertMember(target.slice(1), strval) } else { value = md.type.convert(strval) } @@ -3627,12 +4023,14 @@ function validateSetArgs(key: string, values: string[]) { * @param values The value you wish for, e.g. `next` * * Example: - * - `seturl .*\\.fr followpagepatterns.next suivant` + * - `seturl .*\.fr followpagepatterns.next suivant` * - `seturl website.fr followpagepatterns.next next` * * When multiple patterns can apply to a same URL, the pattern that has the highest priority is used. You can set the priority of a pattern by using `:seturl pattern priority 10`. By default every pattern has a priority of 10. * * Note that the patterns a regex-like, not glob-like. This means that if you want to match everything, you need to use `.*` instead of `*`. + * + * If you'd like to run an ex-command every time a page loads, see [[autocmd]] instead. */ //#content export function seturl(pattern: string, key: string, ...values: string[]) { @@ -3649,6 +4047,30 @@ export function seturl(pattern: string, key: string, ...values: string[]) { return config.setURL(pattern, ...validateSetArgs(key, values)) } +/** + * Usage: `setmode mode key values` + * + * @param mode The Mode the setting should be set for, e.g. `insert` or `ignore`. + * @param key The name of the setting you want to set, e.g. `allowautofocus` + * @param values The value you wish for, e.g. `true` + * + * Currently this command is only supported for the following settings: + * - [[allowautofocus]] + * + * Example: + * - `setmode ignore allowautofocus true` + */ +//#content +export function setmode(mode: string, key: string, ...values: string[]) { + if (!mode || !key || !values.length) { + throw new Error("seturl syntax: mode key value") + } + if (key !== "allowautofocus") + throw new Error("Setting '" + key + "' not supported with setmode") + + return config.set("modesubconfigs", mode, ...validateSetArgs(key, values)) +} + /** Set a key value pair in config. Use to set any values found [here](/static/docs/classes/_src_lib_config_.default_config.html). @@ -3661,8 +4083,6 @@ export function seturl(pattern: string, key: string, ...values: string[]) { If no value is given, the value of the of the key will be displayed. - If the setting you are changing has a dot or period character (.) in it, it cannot be set with `:set` directly. You must either use a helper command for that specific setting - e.g. [[seturl]] or [[autocontain]], or you must use Tridactyl's JavaScript API with `:js tri.config.set("path", "to", "key", "value")` to set `{path: {to: {key: value}}}`. - See also: [[unset]] */ //#background @@ -3719,32 +4139,57 @@ export function firefoxsyncpush() { /** @hidden */ //#background_helper -const AUCMDS = ["DocStart", "DocLoad", "DocEnd", "TriStart", "TabEnter", "TabLeft", "FullscreenChange", "FullscreenEnter", "FullscreenLeft"].concat(webrequests.requestEvents) +const AUCMDS = ["DocStart", "DocLoad", "DocEnd", "TriStart", "TabEnter", "TabLeft", "FullscreenChange", "FullscreenEnter", "FullscreenLeft", "UriChange"].concat(webrequests.requestEvents) /** Set autocmds to run when certain events happen. - - @param event Curently, 'TriStart', 'DocStart', 'DocLoad', 'DocEnd', 'TabEnter', 'TabLeft', 'FullscreenChange', 'FullscreenEnter', 'FullscreenLeft', 'AuthRequired', 'BeforeRedirect', 'BeforeRequest', 'BeforeSendHeaders', 'Completed', 'ErrorOccured', 'HeadersReceived', 'ResponseStarted', and 'SendHeaders' are supported - - @param url For DocStart, DocEnd, TabEnter, and TabLeft: a JavaScript regex (e.g. `www\\.amazon\\.co.*`) - - We just use [URL.search](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search). - - For TriStart: A regular expression that matches the hostname of the computer - the autocmd should be run on. This requires the native messenger to be - installed, except for the ".*" regular expression which will always be - triggered, even without the native messenger. - - For AuthRequired, BeforeRedirect, BeforeRequest, BeforeSendHeaders, Completed, ErrorOccured, HeadersReceived, ResponseStarted and SendHeaders, a [URL match pattern](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns) - - @param excmd The excmd to run (use [[composite]] to run multiple commands), __except__ for AuthRequired, BeforeRedirect, BeforeRequest, BeforeSendHeaders, Completed, ErrorOccured, HeadersReceived, ResponseStarted and SendHeaders, events where it must be an inline JavaScript function which maps [details objects specific to the event](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest#Events) to [blocking responses](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/BlockingResponse). This JavaScript function will run in the background context. - - For example: `autocmd BeforeRequest https://www.bbc.co.uk/* () => ({redirectUrl: "https://old.reddit.com"})`. Note the brackets which ensure JavaScript returns a blocking response object rather than interpreting it as a block statement. - -*/ + * + * @param event Currently, 'TriStart', 'DocStart', 'DocLoad', 'DocEnd', 'TabEnter', 'TabLeft', 'FullscreenChange', 'FullscreenEnter', 'FullscreenLeft', 'UriChange', 'AuthRequired', 'BeforeRedirect', 'BeforeRequest', 'BeforeSendHeaders', 'Completed', 'ErrorOccured', 'HeadersReceived', 'ResponseStarted', and 'SendHeaders' are supported + * + * The 'UriChange' event is for "single page applications" which change their URIs without triggering DocStart or DocLoad events. It uses a timer to check whether the URI has changed, which has a small impact on battery life on pages matching the `url` parameter. We suggest using it sparingly. + * + * @param url For DocStart, DocEnd, TabEnter, and TabLeft: a JavaScript regex (e.g. `www\.amazon\.co.*`) + * + * We just use [URL.search](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search). + * + * For TriStart: A regular expression that matches the hostname of the computer + * the autocmd should be run on. This requires the native messenger to be + * installed, except for the ".*" regular expression which will always be + * triggered, even without the native messenger. + * + * For AuthRequired, BeforeRedirect, BeforeRequest, BeforeSendHeaders, Completed, ErrorOccured, HeadersReceived, ResponseStarted and SendHeaders, a [URL match pattern](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns) + * + * @param excmd The excmd to run (use [[composite]] to run multiple commands), __except__ for AuthRequired, BeforeRedirect, BeforeRequest, BeforeSendHeaders, Completed, ErrorOccured, HeadersReceived, ResponseStarted and SendHeaders, events where it must be an inline JavaScript function which maps [details objects specific to the event](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest#Events) to [blocking responses](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/BlockingResponse). This JavaScript function will run in the background context. + * + * For example: `autocmd BeforeRequest https://www.bbc.co.uk/* () => ({redirectUrl: "https://old.reddit.com"})`. Note the brackets which ensure JavaScript returns a blocking response object rather than interpreting it as a block statement. + * + * For DocStart, DocLoad, DocEnd, TabEnter, TabLeft, FullscreenEnter, FullscreenLeft, FullscreenChange and UriChange: magic variables are available which are replaced with the relevant string at runtime: + - `TRI_FIRED_MOZ_TABID`: Provides Mozilla's `tabID` associated with the fired event. + - `TRI_FIRED_TRI_TABINDEX`: Provides tridactyls internal tab index associated with the fired event. + - `TRI_FIRED_MOZ_WINID`: Provides Mozilla's `windowId` associated with the fired event. + - `TRI_FIRED_MOZ_OPENERTABID`: The ID of the tab that opened this tab. + - `TRI_FIRED_ACTIVE`: Whether the tab is active in its window. This may be true even if the tab's window is not currently focused. + - `TRI_FIRED_AUDIBLE`: Indicates whether the tab is producing sound (even if muted). + - `TRI_FIRED_MUTED`: Indicates whether the tab is muted. + - `TRI_FIRED_DISCARDED`: Whether the tab is discarded. A discarded tab is one whose content has been unloaded from memory. + - `TRI_FIRED_HEIGHT`: The height of the tab in pixels. + - `TRI_FIRED_WIDTH`: The width of the tab in pixels. + - `TRI_FIRED_HIDDEN`: Whether the tab is hidden. + - `TRI_FIRED_INCOGNITO`: Whether the tab is in a private browsing window. + - `TRI_FIRED_ISARTICLE`: True if the tab can be rendered in Reader Mode, false otherwise. + - `TRI_FIRED_LASTACCESSED`: Time at which the tab was last accessed, in milliseconds since the epoch. + - `TRI_FIRED_PINNED`: Whether the tab is pinned. + - `TRI_FIRED_TITLE`: The title of the tab. + - `TRI_FIRED_URL`: The URL of the document that the tab is displaying. + * + * For example: `autocmd DocStart .*example\.com.* zoom 150 false TRI_FIRED_MOZ_TABID`. + * + * For debugging, use `:set logging.autocmds debug` and check the Firefox web console. `WebRequest` events have no logging. + * + */ //#background export function autocmd(event: string, url: string, ...excmd: string[]) { // rudimentary run time type checking // TODO: Decide on autocmd event names - if (!AUCMDS.includes(event)) throw event + " is not a supported event." + if (!AUCMDS.includes(event)) throw new Error(event + " is not a supported event.") return config.set("autocmds", event, url, excmd.join(" ")) } @@ -3754,14 +4199,14 @@ export function autocmd(event: string, url: string, ...excmd: string[]) { * __NB:__ You should use this command with an -s (sane mode) or -u (URL mode) flag. Usage without a flag uses an incorrect regular expression which may cause weird behaviour and has been left in for compatibility reasons. * * This function accepts a `-u` flag to treat the pattern as a URL rather than a domain. - * For example: `autocontain -u ^https?://([^/]*\\.|)youtube\\.com/ google` is equivalent to `autocontain -s youtube\.com google` + * For example: `autocontain -u ^https?://([^/]*\.|)youtube\.com/ google` is equivalent to `autocontain -s youtube\.com google` * * For declaring containers that do not yet exist, consider using `auconcreatecontainer true` in your tridactylrc. * This allows Tridactyl to automatically create containers from your autocontain directives. Note that they will be random icons and colors. * * The domain is passed through as a regular expression so there are a few gotchas to be aware of: - * * Unescaped periods will match *anything*. `autocontain -s google.co.uk work` will match `google!co$uk`. Escape your periods [twice](https://javascript.info/regexp-escaping#new-regexp) (i.e. `\\.` rather than `\.`) or accept that you might get some false positives. - * * You can use regex in your pattern. `autocontain -s google\\.(co\\.uk|com) work` will match either `google.co.uk` or `google.com`. + * * Unescaped periods will match *anything*. `autocontain -s google.co.uk work` will match `google!co$uk`. Escape your periods (i.e. `\.`) or accept that you might get some false positives. + * * You can use regex in your pattern. `autocontain -s google\.(co\.uk|com) work` will match either `google.co.uk` or `google.com`. * * This *should* now peacefully coexist with the Temporary Containers and Multi-Account Containers addons. Do not trust this claim. If a fight starts the participants will try to open infinite tabs. It is *strongly* recommended that you use a tridactylrc so that you can abort a sorceror's-apprentice scenario by killing firefox, commenting out all of autocontainer directives in your rc file, and restarting firefox to clean up the mess. There are a number of strange behaviors resulting from limited coordination between extensions. Redirects can be particularly surprising; for example, with `:autocontain -s will-redirect.example.org example` set and `will-redirect.example.org` redirecting to `redirected.example.org`, navigating to `will-redirect.example.org` will result in the new tab being in the `example` container under some conditions and in the `firefox-default` container under others. * @@ -3794,7 +4239,7 @@ export function autocontain(...args: string[]) { */ //#background export function autocmddelete(event: string, url: string) { - if (!AUCMDS.includes(event)) throw event + " is not a supported event." + if (!AUCMDS.includes(event)) throw new Error(`${event} is not a supported event.`) if (webrequests.requestEvents.includes(event)) { webrequests.unregisterWebRequestAutocmd(event, url) } @@ -3855,7 +4300,7 @@ export async function unbind(...args: string[]) { * * This unbinds `I` in ignore mode on every website the URL of which contains `jupyter`, while keeping the binding active everywhere else. * - * Also see [[bind]], [[bindurl]], [[seturl]], [[unbind]], [[unseturl]] + * Also see [[bind]], [[bindurl]], [[seturl]], [[unbind]], [[unseturl]], [[setmode]], [[unsetmode]] */ //#background export async function unbindurl(pattern: string, mode: string, keys: string) { @@ -3891,6 +4336,8 @@ export async function reset(mode: string, key: string) { * - [[unbindurl]] * - [[seturl]] * - [[unseturl]] + * - [[setmode]] + * - [[unsetmode]] */ //#background export async function reseturl(pattern: string, mode: string, key: string) { @@ -3898,16 +4345,16 @@ export async function reseturl(pattern: string, mode: string, key: string) { return config.unsetURL(pattern, args_obj.configName, args_obj.key) } -/** Deletes various privacy-related items. +/** Deletes various bits of Firefox or Tridactyl data The list of possible arguments can be found here: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/browsingData/DataTypeSet - Additional, tridactyl-specific arguments are: - - commandline: Removes the in-memory commandline history. - - tridactyllocal: Removes all tridactyl storage local to this machine. Use it with + Additional Tridactyl-specific arguments are: + - `commandline`: Removes the in-memory commandline history. + - `tridactyllocal`: Removes all tridactyl storage local to this machine. Use it with commandline if you want to delete your commandline history. - - tridactylsync: Removes all tridactyl storage associated with your Firefox Account (i.e, all user configuration, by default). + - `tridactylsync`: Removes all tridactyl storage associated with your Firefox Account (i.e, all user configuration, by default). These arguments aren't affected by the timespan parameter. Timespan parameter: @@ -3915,7 +4362,7 @@ export async function reseturl(pattern: string, mode: string, key: string) { Examples: - - `sanitise all` -> Deletes everything + - `sanitise all` -> Deletes __everything__, including any saved usernames / passwords(!) - `sanitise history` -> Deletes all history - `sanitise commandline tridactyllocal tridactylsync` -> Deletes every bit of data Tridactyl holds - `sanitise cookies -t 3d` -> Deletes cookies that were set during the last three days. @@ -4016,13 +4463,17 @@ export async function quickmark(key: string, ...addressarr: string[]) { const address = addressarr.length === 0 ? (await activeTab()).url : addressarr[0] // Have to await these or they race! await bind("gn" + key, "tabopen", address) + await sleep(50) await bind("go" + key, "open", address) + await sleep(50) await bind("gw" + key, "winopen", address) } else { const compstring = addressarr.join("; tabopen ") const compstringwin = addressarr.join("; winopen ") await bind("gn" + key, "composite tabopen", compstring) + await sleep(50) await bind("go" + key, "composite open", compstring) + await sleep(50) await bind("gw" + key, "composite winopen", compstringwin) } } @@ -4049,27 +4500,39 @@ export function get(...keys: string[]) { return done } -/** Opens the current configuration in Firefox's native JSON viewer in the current tab. +/** + * Opens the current configuration in Firefox's native JSON viewer in a new tab. * - * NB: Tridactyl cannot run on this page! + * @param key - The specific key you wish to view (e.g, nmaps, autocmds.DocLoad). Also accepts the arguments `--default` or `--user` to view the default configuration, or your changes. * - * @param key - The specific key you wish to view (e.g, nmaps), or `--default` or `--user` to view the default configuration, or your changes. + * NB: the configuration won't update if you refresh the page - you need to run `:viewconfig` again. * */ -//#content -export function viewconfig(key?: string) { +//#background +export function viewconfig(...key: string[]) { // # and white space don't agree with FF's JSON viewer. // Probably other symbols too. - if (!key) window.location.href = "data:application/json," + JSON.stringify(config.get()).replace(/#/g, "%23").replace(/ /g, "%20") + let json + if (key.length === 0) json = config.get() // I think JS casts key to the string "undefined" if it isn't given. - else if (key === "--default") { - window.location.href = "data:application/json," + JSON.stringify(config.o(new config.default_config())).replace(/#/g, "%23").replace(/ /g, "%20") - } else if (key === "--user") { - window.location.href = "data:application/json," + JSON.stringify(config.USERCONFIG).replace(/#/g, "%23").replace(/ /g, "%20") + else if (key[0] === "--default") { + json = key[1] !== undefined ? config.getDeepProperty(config.o(new config.default_config()), key[1].split(".")) : config.o(new config.default_config()) + } else if (key[0] === "--user") { + json = key[1] !== undefined ? config.getDeepProperty(config.USERCONFIG, key[1].split(".")) : config.USERCONFIG + } else { + json = config.getDynamic(...key.join(".").split(".")) } - window.location.href = "data:application/json," + JSON.stringify(config.getDynamic(key)).replace(/#/g, "%23").replace(/ /g, "%20") - // base 64 encoding is a cleverer way of doing this, but it doesn't seem to work for the whole config. - //window.location.href = "data:application/json;base64," + btoa(JSON.stringify(config.get())) + jsonview(JSON.stringify(json)) +} + +/** + * View a JSON object in Firefox's JSON viewer. + */ +//#background +export async function jsonview(...json: string[]) { + const tab = await tabopen("-w", browser.runtime.getURL("static/newtab.html")) + const url = "data:application/json," + encodeURIComponent(json.join(" ")) + return browser.tabs.executeScript(tab.id, { code: `window.location.href = "${url}";` }) } /** @@ -4095,6 +4558,23 @@ export function unseturl(pattern: string, key: string) { return config.unsetURL(pattern, key.split(".")) } +/** + * Reset a mode-specific setting. + * + * usage: `unsetmode mode key` + * + * @param mode The mode the setting should be unset on, e.g. `insert`. + * @param key The key that should be unset. + * + * Example: `unsetmode ignore allowautofocus` + * + * Note that this removes a setting from the mode-specific config, it doesn't "invert" it. This means that if you have a setting set to `false` in your global config and the same setting set to `false` in a mode-specific setting, using `unseturl` will result in the setting still being set to `false`. + */ +//#content +export function unsetmode(mode: string, key: string) { + return config.unset("modesubconfigs", mode, ...key.split(".")) +} + /** * Reset a config setting to default */ @@ -4117,11 +4597,20 @@ export function setnull(...keys: string[]) { // }}} +/** + * @hidden + */ +//#content_helper +const KILL_STACK: Element[] = [] // {{{ HINTMODE /** Hint a page. - @param option + @param args Arguments to the `:hint` command. Multiple flags can be combined as long as they don't conflict. + Selectors can be specified either standalone (without a flag preceding them) or with the `-c` option. Arguments that + take callbacks (`-F` or `-W`) should be specified last, as they consume the rest of the command line. + + Hinting action flags (only one can be specified): - -t open in a new foreground tab - -b open in background - -y copy (yank) link's target to clipboard @@ -4131,31 +4620,42 @@ export function setnull(...keys: string[]) { - -r read an element's text with text-to-speech - -i view an image - -I view an image in a new tab - - -k delete an element from the page + - -k irreversibly deletes an element from the page (until reload) + - -K hides an element on the page; hidden elements can be restored using [[elementunhide]]. - -s save (download) the linked resource - -S save the linked image - -a save-as the linked resource - -A save-as the linked image - -; focus an element and set it as the element or the child of the element to scroll - -# yank an element's anchor URL to clipboard - - -c [selector] hint links that match the css selector - - `bind ;c hint -c [class*="expand"],[class="togg"]` works particularly well on reddit and HN - - this works with most other hint modes, with the caveat that if other hint mode takes arguments your selector must contain no spaces, i.e. `hint -c[yourOtherFlag] [selector] [your other flag's arguments, which may contain spaces]` - - -f [text] hint links and inputs that display the given text - - `bind hint -f Edit` - - -fr [text] use RegExp to hint the links and inputs - -w open in new window - -wp open in new private window - -z scroll an element to the top of the viewport - `-pipe selector key` e.g, `-pipe a href` returns the URL of the chosen link on a page. Only makes sense with `composite`, e.g, `composite hint -pipe .some-class>a textContent | yank`. If you don't select a hint (i.e. press ), will return an empty string. Most useful when used like `-c` to do things other than opening links. NB: the query selector cannot contain any spaces. - `-W excmd...` append hint href to excmd and execute, e.g, `hint -W mpvsafe` to open YouTube videos. NB: appending to bare [[exclaim]] is dangerous - see `get exaliases.mpvsafe` for an example of how to to it safely. If you need to use a query selector, use `-pipe` instead. - - -q* quick (or rapid) hints mode. Stay in hint mode until you press , e.g. `:hint -qb` to open multiple hints in the background or `:hint -qW excmd` to execute excmd once for each hint. This will return an array containing all elements or the result of executed functions (e.g. `hint -qpipe a href` will return an array of links). - - -J* disable javascript hints. Don't generate hints related to javascript events. This is particularly useful when used with the `-c` option when you want to generate only hints for the specified css selectors. Also useful on sites with plenty of useless javascript elements such as google.com - - For example, use `bind ;jg hint -Jc .rc > .r > a` on google.com to generate hints only for clickable search results of a given query - - -br deprecated, use `-qb` instead - -F [callback] - run a custom callback on the selected hint, e.g. `hint -JF e => {tri.excmds.tabopen("-b",e.href); e.remove()}`. - Excepting the custom selector mode and background hint mode, each of these hint modes is available by default as `;