mirror of
https://github.com/vale981/tridactyl
synced 2025-03-05 09:31:41 -05:00
6163 lines
238 KiB
TypeScript
6163 lines
238 KiB
TypeScript
/* eslint-disable spaced-comment */
|
||
// '//#' is a start point for a simple text-replacement-type macro. See excmds_macros.py
|
||
|
||
/** # Tridactyl help page
|
||
|
||
Use `:help <excmd>` or scroll down to show [[help]] for a particular excmd. If you're still stuck, you might consider reading through the [:tutor](/static/clippy/1-tutor.html) again.
|
||
|
||
The default keybinds and settings can be found [here](/static/docs/classes/_src_lib_config_.default_config.html) and active binds can be seen with `:viewconfig nmaps` or with [[bind]].
|
||
|
||
Tridactyl also provides a few functions to manipulate text in the command line or text areas that can be found [here](/static/docs/modules/_src_lib_editor_.html). There are also a few commands only available in the command line which can be found [here](/static/docs/modules/_src_commandline_frame_.html).
|
||
|
||
Ex-commands available exclusively in hint mode are listed [here](/static/docs/modules/_src_content_hinting_.html)
|
||
|
||
We also have a [wiki](https://github.com/tridactyl/tridactyl/wiki) which may be edited by anyone.
|
||
|
||
## How to use this help page
|
||
|
||
Every function (excmd) on this page can be called via Tridactyl's command line which we call "ex". There is a slight change in syntax, however. Wherever you see:
|
||
|
||
`function(arg1,arg2)`
|
||
|
||
You should instead type
|
||
|
||
`function arg1 arg2` into the Tridactyl command line (accessed via `:`)
|
||
|
||
A "splat" operator (...) means that the excmd will accept any number of space-delimited arguments into that parameter.
|
||
|
||
Above each function signature you will see any aliases or key sequences bound to it. The internal names for the various modes are used, which are listed here:
|
||
|
||
- `nmaps`: normal mode binds
|
||
- `imaps`: insert mode binds
|
||
- `inputmaps`: input mode binds
|
||
- `ignoremaps`: ignore mode binds
|
||
- `exaliases`: aliases in the command mode
|
||
- `exmaps`: commandline mode binds
|
||
|
||
At the bottom of each function's help page, you can click on a link that will take you straight to that function's definition in our code.
|
||
|
||
You do not need to worry about types. Return values which are promises will turn into whatever they promise to when used in [[composite]].
|
||
|
||
## 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` 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 [[nativeinstall]]). Alternatively, you can edit your userChrome yourself.
|
||
|
||
## Getting help
|
||
|
||
For more information, and FAQs, check out our [readme][2] and [troubleshooting guide][3] on github.
|
||
|
||
Tridactyl is in a pretty early stage of development. Please report any issues and make requests for missing features on the GitHub [project page][1]. You can also get in touch using Matrix, Gitter, or IRC chat clients:
|
||
|
||
[![Matrix Chat][matrix-badge]][matrix-link]
|
||
[![Gitter Chat][gitter-badge]][gitter-link]
|
||
[![Libera Chat][libera-badge]][libera-link]
|
||
|
||
All three channels are mirrored together, so it doesn't matter which one you use.
|
||
|
||
[1]: https://github.com/tridactyl/tridactyl/issues
|
||
[2]: https://github.com/tridactyl/tridactyl#readme
|
||
[3]: https://github.com/tridactyl/tridactyl/blob/master/doc/troubleshooting.md
|
||
|
||
[gitter-badge]: /static/badges/gitter-badge.svg
|
||
[gitter-link]: https://gitter.im/tridactyl/Lobby
|
||
[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 */
|
||
|
||
// {{{ setup
|
||
|
||
// Shared
|
||
import * as Messaging from "@src/lib/messaging"
|
||
import { ownWinTriIndex, getTriVersion, browserBg, activeTab, activeTabOnWindow, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow, openInTab, queryAndURLwrangler, goToTab, getSortedTabs, prevActiveTab } from "@src/lib/webext"
|
||
import * as Container from "@src/lib/containers"
|
||
import state from "@src/state"
|
||
import * as State from "@src/state"
|
||
import { contentState, ModeName } from "@src/content/state_content"
|
||
import * as UrlUtil from "@src/lib/url_util"
|
||
import * as config from "@src/lib/config"
|
||
import * as aliases from "@src/lib/aliases"
|
||
import * as Logging from "@src/lib/logging"
|
||
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"
|
||
import * as Proxy from "@src/lib/proxy"
|
||
import * as arg from "@src/lib/arg_util"
|
||
|
||
/**
|
||
* This is used to drive some excmd handling in `composite`.
|
||
*
|
||
* @hidden
|
||
*/
|
||
let ALL_EXCMDS
|
||
|
||
// The entry-point script will make sure this has the right set of
|
||
// excmds, so we can use it without futher configuration.
|
||
import * as controller from "@src/lib/controller"
|
||
|
||
//#content_helper
|
||
import { generator as KEY_MUNCHER } from "@src/content/controller_content"
|
||
|
||
/**
|
||
* Used to store the types of the parameters for each excmd for
|
||
* self-documenting functionality.
|
||
*
|
||
* @hidden
|
||
*/
|
||
export const cmd_params = new Map<string, Map<string, string>>()
|
||
|
||
/** @hidden */
|
||
const logger = new Logging.Logger("excmd")
|
||
|
||
/** @hidden **/
|
||
const TRI_VERSION = getTriVersion()
|
||
|
||
//#content_helper
|
||
// {
|
||
import "@src/lib/number.clamp"
|
||
import * as CTSELF from "@src/.excmds_content.generated"
|
||
import { CmdlineCmds as CtCmdlineCmds } from "@src/background/commandline_cmds"
|
||
import { EditorCmds as CtEditorCmds } from "@src/background/editor"
|
||
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 { 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"
|
||
import * as gobbleMode from "@src/parsers/gobblemode"
|
||
import * as nMode from "@src/parsers/nmode"
|
||
|
||
ALL_EXCMDS = {
|
||
"": CTSELF,
|
||
ex: CtCmdlineCmds,
|
||
text: CtEditorCmds,
|
||
}
|
||
// }
|
||
|
||
import { mapstrToKeyseq, mozMapToMinimalKey, minimalKeyToMozMap } from "@src/lib/keyseq"
|
||
|
||
//#background_helper
|
||
// {
|
||
|
||
// tslint:disable-next-line:no-unused-declaration
|
||
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 { EditorCmds } from "@src/background/editor"
|
||
import { firefoxVersionAtLeast } from "@src/lib/webext"
|
||
import { parse_bind_args, modeMaps } from "@src/lib/binding"
|
||
import * as rc from "@src/background/config_rc"
|
||
import * as css_util from "@src/lib/css_util"
|
||
import * as Updates from "@src/lib/updates"
|
||
import * as Extensions from "@src/lib/extension_info"
|
||
import * as webrequests from "@src/background/webrequests"
|
||
import * as commandsHelper from "@src/background/commands"
|
||
import { tgroups, tgroupActivate, setTabTgroup, setWindowTgroup, setTgroups, windowTgroup, windowLastTgroup, tgroupClearOldInfo, tgroupLastTabId, tgroupTabs, clearAllTgroupInfo, tgroupActivateLast, tgroupHandleTabActivated, tgroupHandleTabCreated, tgroupHandleTabAttached, tgroupHandleTabUpdated, tgroupHandleTabRemoved, tgroupHandleTabDetached } from "./lib/tab_groups"
|
||
|
||
ALL_EXCMDS = {
|
||
"": BGSELF,
|
||
ex: BgCmdlineCmds,
|
||
text: BgEditorCmds,
|
||
}
|
||
/** @hidden */
|
||
// }
|
||
|
||
// }}}
|
||
|
||
// {{{ Native messenger stuff
|
||
|
||
/** @hidden **/
|
||
//#background
|
||
export async function getNativeVersion(): Promise<string> {
|
||
return Native.getNativeMessengerVersion()
|
||
}
|
||
|
||
/** @hidden
|
||
* This function is used by rssexec and rssexec completions.
|
||
*/
|
||
//#content
|
||
export async function getRssLinks(): Promise<Array<{ type: string; url: string; title: string }>> {
|
||
const seen = new Set<string>()
|
||
return Array.from(document.querySelectorAll<HTMLAnchorElement | HTMLLinkElement>("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) {
|
||
// if type doesn't match either rss or atom, don't include link
|
||
if (e.type.indexOf("rss") < 0 && e.type.indexOf("atom") < 0) return acc
|
||
type = e.type
|
||
} else {
|
||
// Making sure that we match either a dot or "xml" because "urss" and "atom" are actual words
|
||
if (/(\.rss)|(rss\.xml)/i.test(e.href)) type = "application/rss+xml"
|
||
else if (/(\.atom)|(atom\.xml)/i.test(e.href)) type = "application/atom+xml"
|
||
else return acc
|
||
}
|
||
if (seen.has(e.href)) return acc
|
||
seen.add(e.href)
|
||
return acc.concat({ type, url: e.href, title: e.title || e.innerText } as { type: string; url: string; title: string })
|
||
}, [])
|
||
}
|
||
|
||
/**
|
||
* Execute [[rsscmd]] for an rss link.
|
||
*
|
||
* If `url` is undefined, Tridactyl will look for rss links in the current
|
||
* page. If it doesn't find any, it will display an error message. If it finds
|
||
* multiple urls, it will offer completions in order for you to select the link
|
||
* you're interested in. If a single rss feed is found, it will automatically
|
||
* be selected.
|
||
*/
|
||
//#content
|
||
export async function rssexec(url: string, type?: string, ...title: string[]) {
|
||
if (!url || url === "") {
|
||
const links = await getRssLinks()
|
||
switch (links.length) {
|
||
case 0:
|
||
throw new Error("No rss link found on this page.")
|
||
break
|
||
case 1:
|
||
url = links[0].url
|
||
title = [links[0].title]
|
||
type = links[0].type
|
||
break
|
||
default:
|
||
return fillcmdline("rssexec")
|
||
}
|
||
}
|
||
let rsscmd = config.get("rsscmd")
|
||
if (rsscmd.match("%[uty]")) {
|
||
rsscmd = rsscmd
|
||
.replace("%u", url)
|
||
.replace("%t", title.join(" "))
|
||
.replace("%y", type || "")
|
||
} else {
|
||
rsscmd += " " + url
|
||
}
|
||
// Need actual excmd parsing here.
|
||
return controller.acceptExCmd(rsscmd)
|
||
}
|
||
|
||
/**
|
||
* Fills the element matched by `selector` with content and falls back to the last used input if the element can't be found. You probably don't want this; it used to be used internally for [[editor]].
|
||
*
|
||
* That said, `bind gs fillinput null [Tridactyl](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/) is my favourite add-on` could probably come in handy.
|
||
*/
|
||
//#content
|
||
export function fillinput(selector: string, ...content: string[]) {
|
||
let inputToFill = document.querySelector(selector)
|
||
if (!inputToFill) inputToFill = DOM.getLastUsedInput()
|
||
|
||
// CodeMirror support (I think only versions prior to CodeMirror 6)
|
||
if (inputToFill?.parentNode?.parentElement?.className?.match(/CodeMirror/gi)) {
|
||
;(inputToFill.parentNode.parentElement as any).wrappedJSObject.CodeMirror.setValue(content.join(" "))
|
||
return
|
||
}
|
||
|
||
if ("value" in inputToFill) {
|
||
;(inputToFill as HTMLInputElement).value = content.join(" ")
|
||
} else {
|
||
inputToFill.textContent = content.join(" ")
|
||
}
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content_helper
|
||
export function getInput(e: HTMLElement) {
|
||
// this should probably be subsumed by the focusinput code
|
||
if ("value" in e) {
|
||
return (e as HTMLInputElement).value
|
||
} else {
|
||
return e.textContent
|
||
}
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export function getinput() {
|
||
return getInput(DOM.getLastUsedInput())
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export function getInputSelector() {
|
||
return DOM.getSelector(DOM.getLastUsedInput())
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export function addTridactylEditorClass(selector: string) {
|
||
document.querySelector(selector)?.classList.add("TridactylEditing")
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export function removeTridactylEditorClass(selector: string) {
|
||
document.querySelector(selector)?.classList.remove("TridactylEditing")
|
||
}
|
||
|
||
//#content_helper
|
||
import { getEditor } from "editor-adapter"
|
||
|
||
/**
|
||
* Opens your favourite editor (which is currently gVim) and fills the last used input with whatever you write into that file.
|
||
* **Requires that the native messenger is installed, see [[native]] and [[nativeinstall]]**.
|
||
*
|
||
* Uses the `editorcmd` config option, default = `auto` looks through a list defined in lib/native.ts try find a sensible combination. If it's a bit slow, or chooses the wrong editor, or gives up completely, set editorcmd to something you want. The command must stay in the foreground until the editor exits.
|
||
*
|
||
* The editorcmd needs to accept a filename, stay in the foreground while it's edited, save the file and exit. By default the filename is added to the end of editorcmd, if you require control over the position of that argument, the first occurrence of %f in editorcmd is replaced with the filename. %l, if it exists, is replaced with the line number of the cursor and %c with the column number. For example:
|
||
* ```
|
||
* set editorcmd terminator -u -e "vim %f '+normal!%lGzv%c|'"
|
||
* ```
|
||
*
|
||
* You're probably better off using the default insert mode bind of `<C-i>` (Ctrl-i) to access this.
|
||
*
|
||
* This function returns a tuple containing the path to the file that was opened by the editor and its content. This enables creating commands such as the following one, which deletes the temporary file created by the editor:
|
||
* ```
|
||
* alias editor_rm composite editor | jsb -p tri.native.run(`rm -f '${JS_ARG[0]}'`)
|
||
* bind --mode=insert <C-i> editor_rm
|
||
* bind --mode=input <C-i> editor_rm
|
||
* ```
|
||
*/
|
||
//#content
|
||
export async function editor() {
|
||
const elem = DOM.getLastUsedInput()
|
||
const selector = DOM.getSelector(elem)
|
||
addTridactylEditorClass(selector)
|
||
|
||
if (!(await Native.nativegate())) {
|
||
removeTridactylEditorClass(selector)
|
||
return undefined
|
||
}
|
||
|
||
const beforeUnloadListener = (event: BeforeUnloadEvent) => {
|
||
event.preventDefault()
|
||
event.returnValue = true
|
||
}
|
||
window.addEventListener("beforeunload", beforeUnloadListener)
|
||
|
||
let ans
|
||
try {
|
||
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, ...pos)
|
||
|
||
if (exec.code == 0) {
|
||
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
|
||
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 {
|
||
removeTridactylEditorClass(selector)
|
||
window.removeEventListener("beforeunload", beforeUnloadListener)
|
||
return ans
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Like [[guiset]] but quieter.
|
||
*/
|
||
//#background
|
||
export async function guiset_quiet(rule: string, option: string) {
|
||
if (!rule || !option) throw new Error(":guiset requires two arguments. See `:help guiset` for more information.")
|
||
if (rule == "navbar" && option == "none") throw new Error("`:guiset navbar none` is currently broken, see https://github.com/tridactyl/tridactyl/issues/1728")
|
||
// Could potentially fall back to sending minimal example to clipboard if native not installed
|
||
|
||
// Check for native messenger and make sure we have a plausible profile directory
|
||
if (!(await Native.nativegate("0.1.1"))) return
|
||
const profile_dir = await Native.getProfileDir()
|
||
await setpref("toolkit.legacyUserProfileCustomizations.stylesheets", "true")
|
||
|
||
// Make backups
|
||
await Native.mkdir(profile_dir + "/chrome", true)
|
||
const cssstr = (await Native.read(profile_dir + "/chrome/userChrome.css")).content
|
||
const cssstrOrig = (await Native.read(profile_dir + "/chrome/userChrome.orig.css")).content
|
||
if (cssstrOrig === "") await Native.write(profile_dir + "/chrome/userChrome.orig.css", cssstr)
|
||
await Native.write(profile_dir + "/chrome/userChrome.css.tri.bak", cssstr)
|
||
|
||
// Modify and write new CSS
|
||
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)
|
||
}
|
||
|
||
/**
|
||
* Change which parts of the Firefox user interface are shown. **NB: This feature is experimental and might break stuff.**
|
||
*
|
||
* Might mangle your userChrome. Requires native messenger, and you must restart Firefox each time to see any changes (this can be done using [[restart]]). <!-- (unless you enable addon debugging and refresh using the browser toolbox) -->
|
||
*
|
||
* Also flips the preference `toolkit.legacyUserProfileCustomizations.stylesheets` to true so that FF will read your userChrome.
|
||
*
|
||
* View available rules and options [here](/static/docs/modules/_src_lib_css_util_.html#potentialrules) and [here](/static/docs/modules/_src_lib_css_util_.html#metarules).
|
||
*
|
||
* Example usage: `guiset gui none`, `guiset gui full`, `guiset tabs autohide`.
|
||
*
|
||
* Some of the available options:
|
||
*
|
||
* - gui
|
||
* - full
|
||
* - none
|
||
*
|
||
* - tabs
|
||
* - always
|
||
* - autohide
|
||
*
|
||
* - navbar
|
||
* - always
|
||
* - autohide
|
||
* - none
|
||
*
|
||
* - hoverlink (the little link that appears when you hover over a link)
|
||
* - none
|
||
* - left
|
||
* - right
|
||
* - top-left
|
||
* - top-right
|
||
*
|
||
* - statuspanel (hoverlink + the indicator that appears when a website is loading)
|
||
* - none
|
||
* - left
|
||
* - right
|
||
* - top-left
|
||
* - top-right
|
||
*
|
||
* If you want to use guiset in your tridactylrc, you might want to use [[guiset_quiet]] instead.
|
||
*/
|
||
//#background
|
||
export async function guiset(rule: string, option: string) {
|
||
if (!(await guiset_quiet(rule, option))) {
|
||
throw new Error(":guiset failed. Please ensure native messenger is installed.")
|
||
}
|
||
|
||
return fillcmdline_tmp(3000, "userChrome.css written. Please restart Firefox to see the changes.")
|
||
}
|
||
|
||
/** @hidden */
|
||
//#background
|
||
export function cssparse(...css: string[]) {
|
||
console.log(CSS.parse(css.join(" ")))
|
||
}
|
||
|
||
/** @hidden */
|
||
//#background
|
||
export async function loadtheme(themename: string) {
|
||
if (!(await Native.nativegate("0.1.9"))) return
|
||
const separator = (await browserBg.runtime.getPlatformInfo()).os === "win" ? "\\" : "/"
|
||
// 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) {
|
||
if (Object.keys(await config.get("customthemes")).includes(themename)) return
|
||
throw new Error("Couldn't read theme " + path)
|
||
}
|
||
return set("customthemes." + themename, file.content)
|
||
}
|
||
|
||
/** @hidden */
|
||
//#background
|
||
export async function unloadtheme(themename: string) {
|
||
return unset("customthemes." + themename)
|
||
}
|
||
|
||
/**
|
||
* 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 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(...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}).`)
|
||
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. 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.
|
||
*
|
||
* Note that not all of the keys Firefox uses are suggested by Tridactyl.
|
||
*
|
||
* e.g.: `setpref general.warnOnAboutConfig false`
|
||
*/
|
||
//#background
|
||
export function setpref(key: string, ...value: string[]) {
|
||
return Native.writePref(key, value.join(" "))
|
||
}
|
||
|
||
/**
|
||
* Remove a setting from your user.js file.
|
||
*
|
||
* @param key The key that should be set. Must not be quoted. Must not contain spaces.
|
||
*
|
||
*/
|
||
//#background
|
||
export function removepref(key: string) {
|
||
return Native.removePref(key)
|
||
}
|
||
|
||
/**
|
||
* Like [[fixamo]] but quieter.
|
||
*
|
||
* Now purely a placebo as [[fixamo]] has been removed.
|
||
*/
|
||
//#background
|
||
export async function fixamo_quiet() {
|
||
return logger.warning("fixamo_quiet has been removed at the behest of the Firefox Security team. See :help fixamo for more info.")
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Used to simply set
|
||
* ```js
|
||
* "privacy.resistFingerprinting.block_mozAddonManager":true
|
||
* "extensions.webextensions.restrictedDomains":""
|
||
* ```
|
||
* in about:config via user.js so that Tridactyl (and other extensions!) can be used on addons.mozilla.org and other sites.
|
||
*
|
||
* Removed at the request of the Firefox Security team. Replacements exist in our exemplar RC file.
|
||
*
|
||
* Requires `native` and a `restart`.
|
||
*/
|
||
//#background
|
||
export async function fixamo() {
|
||
fillcmdline_tmp(10000, "fixamo has been removed at the request of the Firefox Security team. Alternatives exist in our exemplar RC file.")
|
||
}
|
||
|
||
/**
|
||
* Uses the native messenger to open URLs.
|
||
*
|
||
* **Be *seriously* careful with this:**
|
||
*
|
||
* 1. the implementation basically execs `firefox --new-tab <your shell escaped string here>`
|
||
* 2. you can use it to open any URL you can open in the Firefox address bar,
|
||
* including ones that might cause side effects (firefox does not guarantee
|
||
* that about: pages ignore query strings).
|
||
*
|
||
* You've been warned.
|
||
*
|
||
* This uses the [[browser]] setting to know which binary to call. If you need to pass additional arguments to firefox (e.g. '--new-window'), make sure they appear before the url.
|
||
*/
|
||
//#background
|
||
export async function nativeopen(...args: string[]) {
|
||
const index = args.findIndex(arg => !arg.startsWith("-"))
|
||
let firefoxArgs = []
|
||
if (index >= 0) {
|
||
firefoxArgs = args.slice(0, index)
|
||
}
|
||
const url = args.slice(firefoxArgs.length).join(" ")
|
||
|
||
if (await Native.nativegate()) {
|
||
// First compute where the tab should be
|
||
const pos = await config.getAsync("tabopenpos")
|
||
let index = (await activeTab()).index + 1
|
||
switch (pos) {
|
||
case "last":
|
||
index = -1
|
||
break
|
||
case "related":
|
||
// How do we simulate that?
|
||
break
|
||
}
|
||
// Then make sure the tab is made active and moved to the right place
|
||
// when it is opened in the current window
|
||
const selecttab = tab => {
|
||
browser.tabs.onCreated.removeListener(selecttab)
|
||
tabSetActive(tab.id)
|
||
browser.tabs.move(tab.id, { index })
|
||
}
|
||
browser.tabs.onCreated.addListener(selecttab)
|
||
|
||
try {
|
||
if ((await browser.runtime.getPlatformInfo()).os === "mac") {
|
||
if ((await browser.windows.getCurrent()).incognito) {
|
||
throw new Error("nativeopen isn't supported in private mode on OSX. Consider installing Linux or Windows :).")
|
||
}
|
||
const osascriptArgs = ["-e 'on run argv'", "-e 'tell application \"Firefox\" to open location item 1 of argv'", "-e 'end run'"]
|
||
await Native.run("osascript " + osascriptArgs.join(" ") + " " + url)
|
||
} else {
|
||
const os = (await browser.runtime.getPlatformInfo()).os
|
||
if (firefoxArgs.length === 0) {
|
||
try {
|
||
const profile = await Native.getProfile()
|
||
if (profile.Name !== undefined) {
|
||
if (os === "win") {
|
||
firefoxArgs = [`-p "${profile.Name}"`]
|
||
} else {
|
||
firefoxArgs = [`-p '${profile.Name}'`]
|
||
}
|
||
} else if (profile.absolutePath !== undefined) {
|
||
if (os === "win") {
|
||
firefoxArgs = [`--profile "${profile.absolutePath}"`]
|
||
} else {
|
||
firefoxArgs = [`--profile '${profile.absolutePath}'`]
|
||
}
|
||
}
|
||
} catch (e) {
|
||
logger.debug(e)
|
||
firefoxArgs = []
|
||
}
|
||
firefoxArgs.push("--new-tab")
|
||
}
|
||
let escapedUrl: string
|
||
// We need to quote and escape single quotes in the
|
||
// url, otherwise an attacker could create an anchor with a url
|
||
// like 'file:// && $(touch /tmp/dead)' and achieve remote code
|
||
// execution when the user tries to follow it with `hint -W tabopen`
|
||
if (os === "win") {
|
||
escapedUrl = escape.windows_cmd(url)
|
||
} else {
|
||
escapedUrl = escape.sh(url)
|
||
}
|
||
await Native.run(`${config.get("browser")} ${firefoxArgs.join(" ")} ${escapedUrl}`)
|
||
}
|
||
setTimeout(() => browser.tabs.onCreated.removeListener(selecttab), 100)
|
||
} catch (e) {
|
||
browser.tabs.onCreated.removeListener(selecttab)
|
||
throw e
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Run command in /bin/sh (unless you're on Windows), and print the output in the command line. Non-zero exit codes and stderr are ignored, currently.
|
||
*
|
||
* Requires the native messenger, obviously.
|
||
*
|
||
* If you're using `exclaim` with arguments coming from a pipe, consider using [[shellescape]] to properly escape arguments and to prevent unsafe commands.
|
||
*
|
||
* If you want to use a different shell, just prepend your command with whatever the invocation is and keep in mind that most shells require quotes around the command to be executed, e.g. `:exclaim xonsh -c "1+2"`.
|
||
*
|
||
* Aliased to `!` but the exclamation mark **must be followed with a space**.
|
||
*/
|
||
//#background
|
||
export async function exclaim(...str: string[]) {
|
||
let done = Promise.resolve()
|
||
if (await Native.nativegate()) {
|
||
done = fillcmdline((await Native.run(str.join(" "))).content)
|
||
}
|
||
return done
|
||
} // should consider how to give option to fillcmdline or not. We need flags.
|
||
|
||
/**
|
||
* Like exclaim, but without any output to the command line.
|
||
*/
|
||
//#background
|
||
export async function exclaim_quiet(...str: string[]) {
|
||
let result = ""
|
||
if (await Native.nativegate()) {
|
||
result = (await Native.run(str.join(" "))).content
|
||
}
|
||
return result
|
||
}
|
||
|
||
/**
|
||
* Tells you if the native messenger is installed and its version.
|
||
*
|
||
* For snap, flatpak, and other sandboxed installations, additional setup is required – see https://github.com/tridactyl/tridactyl#extra-features-through-native-messaging.
|
||
*/
|
||
//#background
|
||
export async function native() {
|
||
const version = await Native.getNativeMessengerVersion(true)
|
||
let done
|
||
if (version !== undefined) {
|
||
done = fillcmdline("# Native messenger is correctly installed, version " + version)
|
||
} else {
|
||
done = fillcmdline("# Native messenger not found. Please run `:nativeinstall` and follow the instructions.")
|
||
}
|
||
return done
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
*
|
||
* For snap, flatpak, and other sandboxed installations, additional setup is required – see https://github.com/tridactyl/tridactyl#extra-features-through-native-messaging.
|
||
*/
|
||
//#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") {
|
||
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 {
|
||
done = fillcmdline("# Installation command copied to clipboard. Please paste and run it in your shell to install the native messenger.")
|
||
}
|
||
return done
|
||
}
|
||
|
||
/** Writes current config to a file.
|
||
|
||
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
|
||
excmd excluding the `-f` flag will be treated as a path to write the rc
|
||
file to relative to the native messenger's location (`~/.local/share/tridactyl/`). By default, it silently refuses to overwrite existing files.
|
||
|
||
The RC file will be split into sections that will be created if a config
|
||
property is discovered within one of them:
|
||
- General settings
|
||
- Binds
|
||
- Aliases
|
||
- Autocmds
|
||
- Autocontainers
|
||
- Logging
|
||
|
||
Note:
|
||
- Subconfig paths fall back to using `js tri.config.set(key: obj)` notation.
|
||
- This method is also used as a fallback mechanism for objects that didn't hit
|
||
any of the heuristics.
|
||
|
||
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.
|
||
|
||
*/
|
||
//#background
|
||
export async function mktridactylrc(...args: string[]) {
|
||
let overwrite = false
|
||
|
||
const argParse = (args: string[]): string[] => {
|
||
if (args[0] === "-f") {
|
||
overwrite = true
|
||
args.shift()
|
||
argParse(args)
|
||
}
|
||
return args
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
/**
|
||
* Runs an RC file from disk or a URL
|
||
*
|
||
* This function accepts flags: `--url`, `--clipboard` or `--strings`.
|
||
*
|
||
* If no argument given, it will try to open ~/.tridactylrc, ~/.config/tridactyl/tridactylrc or $XDG_CONFIG_HOME/tridactyl/tridactylrc in reverse order. You may use a `_` in place of a leading `.` if you wish, e.g, if you use Windows.
|
||
*
|
||
* On Windows, the `~` expands to `%USERPROFILE%`.
|
||
*
|
||
* The `--url` flag will load the RC from the URL. If no url is specified with the `--url` flag, the current page's URL is used to locate the RC file. Ensure the URL you pass (or page you are on) is a "raw" RC file, e.g. https://raw.githubusercontent.com/tridactyl/tridactyl/master/.tridactylrc and not https://github.com/tridactyl/tridactyl/blob/master/.tridactylrc.
|
||
*
|
||
* Tridactyl won't run on many raw pages due to a Firefox bug with Content Security Policy, so you may need to use the `source --url [URL]` form.
|
||
*
|
||
* The `--clipboard` flag will load the RC from the clipboard, which is useful for people cannot install the native messenger or do not wish to store their RC online. You can use this with `mktridactylrc --clipboard`.
|
||
*
|
||
* The `--strings` flag will load the RC from rest arguments. It could be useful if you want to execute a batch of commands in js context. Eg: `js tri.excmds.source("--strings", [cmd1, cmd2].join("\n"))`.
|
||
*
|
||
* 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 ~.
|
||
*/
|
||
//#background
|
||
export async function source(...args: string[]) {
|
||
if (args[0] === "--url") {
|
||
let url = args[1]
|
||
if (!url || url === "%") url = window.location.href
|
||
if (!new RegExp("^(https?://)|data:").test(url)) url = "http://" + url
|
||
await rc.sourceFromUrl(url)
|
||
} else if (args[0] === "--strings") {
|
||
await rc.runRc(args.slice(1).join(" "))
|
||
} else if (args[0] === "--clipboard") {
|
||
const text = await getclip()
|
||
await rc.runRc(text)
|
||
} else {
|
||
const file = args.join(" ") || undefined
|
||
if ((await Native.nativegate("0.1.3")) && !(await rc.source(file))) {
|
||
logger.error("Could not find RC file")
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Same as [[source]] but suppresses all errors
|
||
*/
|
||
//#background
|
||
export async function source_quiet(...args: string[]) {
|
||
try {
|
||
await source(...args)
|
||
} catch (e) {
|
||
logger.info("Automatic loading of RC file failed.")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Updates the native messenger if it is installed, using our GitHub repo. This is run every time Tridactyl is updated.
|
||
*
|
||
* If you want to disable this, or point it to your own native messenger, edit the `nativeinstallcmd` setting.
|
||
*/
|
||
//#background
|
||
export async function updatenative(interactive = true) {
|
||
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()
|
||
}
|
||
|
||
/**
|
||
* Restarts firefox with the same commandline arguments.
|
||
*
|
||
* Warning: This can kill your tabs, especially if you :restart several times
|
||
* in a row
|
||
*/
|
||
//#background
|
||
export async function restart() {
|
||
const profiledir = await Native.getProfileDir()
|
||
const browsercmd = await config.get("browser")
|
||
|
||
if ((await browser.runtime.getPlatformInfo()).os === "win") {
|
||
const reply = await Native.winFirefoxRestart(profiledir, browsercmd)
|
||
logger.info("[+] win_firefox_restart 'reply' = " + JSON.stringify(reply))
|
||
if (Number(reply.code) === 0) {
|
||
fillcmdline("#" + reply.content)
|
||
qall()
|
||
} else {
|
||
fillcmdline("#" + reply.error)
|
||
}
|
||
} else {
|
||
const firefox = (await Native.ff_cmdline()).join(" ")
|
||
// Wait for the lock to disappear, then wait a bit more, then start firefox
|
||
Native.run(`while readlink ${profiledir}/lock ; do sleep 1 ; done ; sleep 1 ; ${firefox}`)
|
||
qall()
|
||
}
|
||
}
|
||
|
||
/** Download the current document.
|
||
*
|
||
* 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).
|
||
* If filename is not given, a download dialogue will be opened. If filename is a directory, the file will be saved inside of it, its name being inferred from the URL. If the directories mentioned in the path do not exist or if a file already exists at this path, the file will be kept in your downloads folder and an error message will be given.
|
||
*
|
||
* **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.
|
||
*
|
||
* 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(...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)
|
||
}
|
||
}
|
||
|
||
// }}}
|
||
|
||
/** @hidden */
|
||
//#background_helper
|
||
function tabSetActive(id: number) {
|
||
return browser.tabs.update(id, { active: true })
|
||
}
|
||
|
||
// }}}
|
||
|
||
// {{{ PAGE CONTEXT
|
||
|
||
/** @hidden */
|
||
//#content_helper
|
||
let JUMPED: boolean
|
||
|
||
/** @hidden */
|
||
//#content_helper
|
||
let JUMP_TIMEOUTID: number | undefined
|
||
|
||
/** This is used as an ID for the current page in the jumplist.
|
||
It has a potentially confusing behavior: if you visit site A, then site B, then visit site A again, the jumplist that was created for your first visit on A will be re-used for your second visit.
|
||
An ideal solution would be to have a counter that is incremented every time a new page is visited within the tab and use that as the return value for getJumpPageId but this doesn't seem to be trivial to implement.
|
||
@hidden
|
||
*/
|
||
//#content_helper
|
||
export function getJumpPageId() {
|
||
return document.location.href
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content_helper
|
||
export async function saveJumps(jumps) {
|
||
return browserBg.sessions.setTabValue(await activeTabId(), "jumps", jumps)
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content_helper
|
||
export async function saveTabHistory(history) {
|
||
return browserBg.sessions.setTabValue(await activeTabId(), "history", history)
|
||
}
|
||
|
||
/** Returns a promise for an object with history list, index of a current, previous and next pages */
|
||
/** @hidden */
|
||
//#content_helper
|
||
export async function curTabHistory() {
|
||
const tabid = await activeTabId()
|
||
return await browserBg.sessions.getTabValue(tabid, "history")
|
||
}
|
||
|
||
/** Returns a promise for an object containing the jumplist of all pages accessed in the current tab.
|
||
The keys of the object currently are the page's URL, however this might change some day. Use [[getJumpPageId]] to access the jumplist of a specific page.
|
||
@hidden
|
||
*/
|
||
//#content_helper
|
||
export async function curJumps() {
|
||
const tabid = await activeTabId()
|
||
let jumps = await browserBg.sessions.getTabValue(tabid, "jumps")
|
||
if (!jumps) jumps = {}
|
||
// This makes sure that `key` exists in `obj`, setting it to `def` if it doesn't
|
||
const ensure = (obj, key, def) => {
|
||
if (obj[key] === null || obj[key] === undefined) obj[key] = def
|
||
}
|
||
const page = getJumpPageId()
|
||
ensure(jumps, page, {})
|
||
ensure(jumps[page], "list", [{ x: window.scrollX, y: window.scrollY }])
|
||
ensure(jumps[page], "cur", 0)
|
||
saveJumps(jumps)
|
||
return jumps
|
||
}
|
||
|
||
/** Calls [[jumpprev]](-n) */
|
||
//#content
|
||
export function jumpnext(n = 1) {
|
||
return jumpprev(-n)
|
||
}
|
||
|
||
/** Similar to Pentadactyl or vim's jump list.
|
||
*
|
||
* When you scroll on a page, either by using the mouse or Tridactyl's key bindings, your position in the page will be saved after jumpdelay milliseconds (`:get jumpdelay` to know how many milliseconds that is). If you scroll again, you'll be able to go back to your previous position by using `:jumpprev 1`. If you need to go forward in the jumplist, use `:jumpprev -1`.
|
||
*
|
||
* Known bug: Tridactyl will use the same jumplist for multiple visits to a same website in the same tab, see [github issue 834](https://github.com/tridactyl/tridactyl/issues/834).
|
||
*/
|
||
//#content
|
||
export function jumpprev(n = 1) {
|
||
return curJumps().then(alljumps => {
|
||
const jumps = alljumps[getJumpPageId()]
|
||
const current = jumps.cur - n
|
||
if (current < 0) {
|
||
jumps.cur = 0
|
||
saveJumps(alljumps)
|
||
return back(-current + "")
|
||
} else if (current >= jumps.list.length) {
|
||
jumps.cur = jumps.list.length - 1
|
||
saveJumps(alljumps)
|
||
return forward(current - jumps.list.length + 1 + "")
|
||
}
|
||
jumps.cur = current
|
||
const p = jumps.list[jumps.cur]
|
||
saveJumps(alljumps)
|
||
JUMPED = true
|
||
window.scrollTo(p.x, p.y)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Jumps to a local mark, a global mark, or the location before the last mark jump.
|
||
* [a-z] are local marks, [A-Z] are global marks and '`' is the location before the last mark jump.
|
||
* @param key the key associated with the mark
|
||
*/
|
||
//#content
|
||
export async function markjump(key: string) {
|
||
if (key.length !== 1) {
|
||
throw new Error("markjump accepts only a single letter or '`'")
|
||
}
|
||
if (key === "`") {
|
||
return markjumpbefore()
|
||
}
|
||
if (!/[a-z]/i.exec(key)) {
|
||
throw new Error("markjump accepts only a single letter or '`'")
|
||
}
|
||
if (key === key.toUpperCase()) {
|
||
return markjumpglobal(key)
|
||
}
|
||
return markjumplocal(key)
|
||
}
|
||
|
||
/**
|
||
* Jumps to a local mark.
|
||
* @param key the key associated with the mark
|
||
*/
|
||
//#content
|
||
export async function markjumplocal(key: string) {
|
||
const urlWithoutAnchor = window.location.href.split("#")[0]
|
||
const localMarks = await State.getAsync("localMarks")
|
||
const mark = localMarks.get(urlWithoutAnchor)?.get(key)
|
||
if (mark) {
|
||
const currentTabId = await activeTabId()
|
||
state.beforeJumpMark = { url: urlWithoutAnchor, scrollX: window.scrollX, scrollY: window.scrollY, tabId: currentTabId }
|
||
scrolltab(currentTabId, mark.scrollX, mark.scrollY, `# marks: jumped to mark '${key}'`)
|
||
}
|
||
return fillcmdline_tmp(3000, `# marks: warning - local mark '${key}' is not set in this tab`)
|
||
}
|
||
|
||
/**
|
||
* Jumps to a global mark. If the tab with the mark no longer exists or its url differs from the mark's url,
|
||
* jumps to another tab with the mark's url or creates it first if such tab does not exist.
|
||
* @param key the key associated with the mark
|
||
*/
|
||
//#content
|
||
export async function markjumpglobal(key: string) {
|
||
const globalMarks = await State.getAsync("globalMarks")
|
||
const mark = globalMarks.get(key)
|
||
if (!mark) {
|
||
return fillcmdline_tmp(3000, `# marks: warning - global mark '${key}' is not set`)
|
||
}
|
||
const currentTabId = await activeTabId()
|
||
state.beforeJumpMark = {
|
||
url: window.location.href.split("#")[0],
|
||
scrollX: window.scrollX,
|
||
scrollY: window.scrollY,
|
||
tabId: currentTabId,
|
||
}
|
||
try {
|
||
const tab = await browserBg.tabs.get(mark.tabId)
|
||
return onTabExists(tab)
|
||
} catch (e) {
|
||
return onTabNoLongerValid()
|
||
}
|
||
|
||
async function onTabExists(tab) {
|
||
const tabUrl = tab.url.split("#")[0]
|
||
if (mark.url !== tabUrl) {
|
||
return onTabNoLongerValid()
|
||
}
|
||
return goToTab(tab.id).then(() => {
|
||
scrolltab(tab.id, mark.scrollX, mark.scrollY, `# marks: jumped to mark '${key}'`)
|
||
})
|
||
}
|
||
|
||
// the tab with mark's tabId doesn't exist or it has a different url than the mark's url
|
||
async function onTabNoLongerValid() {
|
||
const matchingTabs = await browserBg.tabs.query({ url: mark.url })
|
||
// If there are no matching tabs, open a new one and update the mark's tabId for future use in this session
|
||
if (!matchingTabs.length) {
|
||
// This (and only this) needs to run in the background
|
||
return tabopenwait(mark.url).then(updateMarkAndScroll)
|
||
}
|
||
// If there are multiple tabs open with the same url, just pick the first one and update the mark's tabId
|
||
// for future use in this session
|
||
return goToTab(matchingTabs[0].id).then(updateMarkAndScroll)
|
||
}
|
||
|
||
function updateMarkAndScroll(tab) {
|
||
mark.tabId = tab.id
|
||
state.globalMarks = globalMarks
|
||
scrolltab(tab.id, mark.scrollX, mark.scrollY, `# marks: jumped to mark '${key}'`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Jumps to a location saved before the last mark jump as long as the tab it's located in exists and its url didn't change.
|
||
* Overwrites the location before the last mark jump - repeating this method will jump back and forth between two locations.
|
||
*/
|
||
//#content
|
||
export async function markjumpbefore() {
|
||
const beforeJumpMark = await State.getAsync("beforeJumpMark")
|
||
if (!beforeJumpMark) {
|
||
return
|
||
}
|
||
try {
|
||
const tab = await browserBg.tabs.get(beforeJumpMark.tabId)
|
||
const tabUrl = tab.url.split("#")[0]
|
||
const { url, scrollX, scrollY, tabId } = beforeJumpMark
|
||
if (url !== tabUrl) {
|
||
return
|
||
}
|
||
const currentTabId = await activeTabId()
|
||
state.beforeJumpMark = { url: window.location.href.split("#")[0], scrollX: window.scrollX, scrollY: window.scrollY, tabId: currentTabId }
|
||
goToTab(tabId).then(() => scrolltab(tabId, scrollX, scrollY, "# marks: jumped back"))
|
||
} catch (e) {
|
||
// the mark's tab is no longer valid
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Scrolls to a given position in a tab identified by tabId and prints a message in it.
|
||
*/
|
||
//#content
|
||
export async function scrolltab(tabId: number, scrollX: number, scrollY: number, message: string) {
|
||
await Messaging.messageTab(tabId, "controller_content", "acceptExCmd", [`scrollto ${scrollX} ${scrollY}`])
|
||
Messaging.messageTab(tabId, "controller_content", "acceptExCmd", [`fillcmdline_tmp 3000 ${message}`])
|
||
}
|
||
|
||
/**
|
||
* Adds a global or a local mark. In case of a local mark, it will be assigned to the current page url.
|
||
* If a mark is already assigned, it is overwritten.
|
||
* @param key the key associated with the mark
|
||
*/
|
||
//#background
|
||
export async function markadd(key: string) {
|
||
if ((await browser.windows.getCurrent()).incognito) {
|
||
throw new Error("Marks cannot be set in private mode")
|
||
}
|
||
// TODO: i18n: this should only ban numbers, not e.g. cyrillic
|
||
if (!/[a-z]/i.exec(key) || key.length !== 1) {
|
||
throw new Error("markadd accepts only a single letter")
|
||
}
|
||
if (key === key.toUpperCase()) {
|
||
return markaddglobal(key)
|
||
}
|
||
return markaddlocal(key)
|
||
}
|
||
|
||
/**
|
||
* Assigns a local mark to the current url and the given key. If a mark is already assigned, it is overwritten.
|
||
* Two urls are considered the same if they're identical ignoring anchors.
|
||
* Local marks are not persisted between browser restarts.
|
||
*/
|
||
//#content
|
||
export async function markaddlocal(key: string) {
|
||
const urlWithoutAnchor = window.location.href.split("#")[0]
|
||
const localMarks = await State.getAsync("localMarks")
|
||
const localUrlMarks = localMarks.get(urlWithoutAnchor) ? localMarks.get(urlWithoutAnchor) : new Map()
|
||
const newLocalMark = { scrollX: window.scrollX, scrollY: window.scrollY }
|
||
localUrlMarks.set(key, newLocalMark)
|
||
localMarks.set(urlWithoutAnchor, localUrlMarks)
|
||
state.localMarks = localMarks
|
||
fillcmdline_tmp(3000, `# marks: local mark '${key}' set`)
|
||
}
|
||
|
||
/**
|
||
* Assigns a global mark to the given key. If a mark is already assigned, it is overwritten.
|
||
* Global marks are persisted between browser restarts.
|
||
*/
|
||
//#content
|
||
export async function markaddglobal(key: string) {
|
||
const urlWithoutAnchor = window.location.href.split("#")[0]
|
||
const globalMarks = await State.getAsync("globalMarks")
|
||
const tabId = await activeTabId()
|
||
const newGlobalMark = { url: urlWithoutAnchor, scrollX: window.scrollX, scrollY: window.scrollY, tabId }
|
||
globalMarks.set(key, newGlobalMark)
|
||
state.globalMarks = globalMarks
|
||
fillcmdline_tmp(3000, `# marks: global mark '${key}' set`)
|
||
}
|
||
|
||
/** Called on 'scroll' events.
|
||
If you want to have a function that moves within the page but doesn't add a
|
||
location to the jumplist, make sure to set JUMPED to true before moving
|
||
around.
|
||
The setTimeout call is required because sometimes a user wants to move
|
||
somewhere by pressing 'j' multiple times and we don't want to add the
|
||
in-between locations to the jump list
|
||
@hidden
|
||
*/
|
||
//#content_helper
|
||
export function addJump() {
|
||
if (JUMPED) {
|
||
JUMPED = false
|
||
return
|
||
}
|
||
const { scrollX, scrollY } = window
|
||
// Prevent pending jump from being registered
|
||
clearTimeout(JUMP_TIMEOUTID)
|
||
// Schedule the registering of the current jump
|
||
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
|
||
if (localTimeoutID !== JUMP_TIMEOUTID) return
|
||
|
||
const jumps = alljumps[getJumpPageId()]
|
||
const list = jumps.list
|
||
// if the page hasn't moved, stop
|
||
if (list[jumps.cur].x === scrollX && list[jumps.cur].y === scrollY) return
|
||
// Store the new jump
|
||
// Could removing all jumps from list[cur] to list[list.length] be
|
||
// a better/more intuitive behavior?
|
||
list.push({ x: scrollX, y: scrollY })
|
||
jumps.cur = jumps.list.length - 1
|
||
saveJumps(alljumps)
|
||
}, config.get("jumpdelay"))
|
||
JUMP_TIMEOUTID = localTimeoutID
|
||
}
|
||
|
||
//#content_helper
|
||
document.addEventListener("scroll", addJump, { passive: true })
|
||
|
||
// Try to restore the previous jump position every time a page is loaded
|
||
//#content_helper
|
||
document.addEventListener("load", () => curJumps().then(() => jumpprev(0)))
|
||
|
||
// Adds a new entry to history tree or updates it if already visited
|
||
/** @hidden */
|
||
//#content_helper
|
||
export async function addTabHistory() {
|
||
let pages = await curTabHistory()
|
||
if (!pages)
|
||
pages = {
|
||
current: null,
|
||
list: [],
|
||
}
|
||
const link = getJumpPageId()
|
||
const current = pages["list"].findIndex(item => item.href === link)
|
||
if (current !== -1) {
|
||
pages["current"] = current
|
||
pages["list"][current].time = Date.now()
|
||
} else {
|
||
pages["list"].push({
|
||
parent: pages["current"],
|
||
href: link,
|
||
title: document.title,
|
||
id: pages["list"].length,
|
||
time: Date.now(),
|
||
})
|
||
pages["current"] = pages["list"].length - 1
|
||
}
|
||
saveTabHistory(pages)
|
||
}
|
||
|
||
// Calls addTabHistory on page load
|
||
/** @hidden */
|
||
//#content_helper
|
||
addTabHistory()
|
||
//#content_helper
|
||
window.addEventListener("HistoryState", addTabHistory)
|
||
|
||
/** Blur (unfocus) the active element and enter normal mode */
|
||
//#content
|
||
export function unfocus() {
|
||
;((document.activeElement.shadowRoot ? DOM.deepestShadowRoot(document.activeElement.shadowRoot) : document).activeElement as HTMLInputElement).blur()
|
||
contentState.mode = "normal"
|
||
}
|
||
|
||
/** Scrolls the window or any scrollable child element by a pixels on the horizontal axis and b pixels on the vertical axis.
|
||
*/
|
||
//#content
|
||
export async function scrollpx(a: number, b: number) {
|
||
let done = Promise.resolve(undefined as any)
|
||
if (!(await scrolling.scroll(a, b, document.documentElement))) {
|
||
done = scrolling.recursiveScroll(a, b)
|
||
}
|
||
return done.then(() => undefined)
|
||
}
|
||
|
||
/** If two numbers are given, treat as x and y values to give to window.scrollTo
|
||
If one number is given, scroll to that percentage along a chosen axis, defaulting to the y-axis. If the number has 'c' appended to it, it will be interpreted in radians.
|
||
|
||
Note that if `a` is 0 or 100 and if the document is not scrollable in the given direction, Tridactyl will attempt to scroll the first scrollable element until it reaches the very bottom of that element.
|
||
|
||
Examples:
|
||
|
||
- `scrollto 50` -> scroll halfway down the page.
|
||
- `scrollto 3.14c` -> scroll approximately 49.97465213% of the way down the page.
|
||
*/
|
||
//#content
|
||
export function scrollto(a: number | string, b: number | "x" | "y" = "y") {
|
||
if (typeof a === "string" && /c$/i.exec(a)) {
|
||
a = (Number(a.replace(/c$/, "")) * 100) / (2 * Math.PI)
|
||
}
|
||
a = Number(a)
|
||
const elem = window.document.scrollingElement || window.document.documentElement
|
||
const percentage = a.clamp(0, 100)
|
||
let done = Promise.resolve(undefined as any)
|
||
if (b === "y") {
|
||
const top = elem.getClientRects()[0].top
|
||
window.scrollTo(window.scrollX, (percentage * elem.scrollHeight) / 100)
|
||
if (top === elem.getClientRects()[0].top && (percentage === 0 || percentage === 100)) {
|
||
// scrollTo failed, if the user wants to go to the top/bottom of
|
||
// the page try scrolling.recursiveScroll instead
|
||
done = scrolling.recursiveScroll(window.scrollX, 1073741824 * (percentage === 0 ? -1 : 1), document.documentElement)
|
||
}
|
||
} else if (b === "x") {
|
||
const left = elem.getClientRects()[0].left
|
||
window.scrollTo((percentage * elem.scrollWidth) / 100, window.scrollY)
|
||
if (left === elem.getClientRects()[0].left && (percentage === 0 || percentage === 100)) {
|
||
done = scrolling.recursiveScroll(1073741824 * (percentage === 0 ? -1 : 1), window.scrollX, document.documentElement)
|
||
}
|
||
} else {
|
||
window.scrollTo(a, Number(b)) // a,b numbers
|
||
}
|
||
return done.then(() => undefined)
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content_helper
|
||
let lineHeight = null
|
||
/** Scrolls the document of its first scrollable child element by n lines.
|
||
*
|
||
* The height of a line is defined by the site's CSS. If Tridactyl can't get it, it'll default to 22 pixels.
|
||
*/
|
||
//#content
|
||
export function scrollline(n = 1, mult = 1) {
|
||
n = mult * n
|
||
if (lineHeight === null) {
|
||
const getLineHeight = elem => {
|
||
// Get line height
|
||
const cssHeight = window.getComputedStyle(elem).getPropertyValue("line-height")
|
||
// Remove the "px" at the end
|
||
return parseInt(cssHeight.substr(0, cssHeight.length - 2), 10)
|
||
}
|
||
lineHeight = getLineHeight(document.documentElement)
|
||
if (!lineHeight) lineHeight = getLineHeight(document.body)
|
||
// Is there a better way to compute a fallback? Maybe fetch from about:preferences?
|
||
if (!lineHeight) lineHeight = 22
|
||
}
|
||
return scrolling.recursiveScroll(0, lineHeight * n)
|
||
}
|
||
|
||
/** Scrolls the document by n pages.
|
||
* 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 `<C-F>` etc., not really useful otherwise.
|
||
*/
|
||
//#content
|
||
export function scrollpage(n = 1, count = 1) {
|
||
return scrollpx(0, window.innerHeight * n * count)
|
||
}
|
||
|
||
/**
|
||
* Rudimentary find mode, left unbound by default as we don't currently support `incsearch`. Suggested binds:
|
||
*
|
||
* bind / fillcmdline find
|
||
* bind ? fillcmdline find --reverse
|
||
* bind n findnext --search-from-view
|
||
* bind N findnext --search-from-view --reverse
|
||
* bind gn findselect
|
||
* bind gN composite findnext --search-from-view --reverse; findselect
|
||
* bind ,<Space> nohlsearch
|
||
*
|
||
* Argument: A string you want to search for.
|
||
*
|
||
* This function accepts two flags: `-?` or `--reverse` to search from the bottom rather than the top and `-: n` or `--jump-to n` to jump directly to the nth match.
|
||
*
|
||
* The behavior of this function is affected by the following setting:
|
||
*
|
||
* `findcase`: either "smart", "sensitive" or "insensitive". If "smart", find will be case-sensitive if the pattern contains uppercase letters.
|
||
*
|
||
* Known bugs: find will currently happily jump to a non-visible element, and pressing n or N without having searched for anything will cause an error.
|
||
*/
|
||
//#content
|
||
export function find(...args: string[]) {
|
||
const argOpt = arg.lib(
|
||
{
|
||
"--jump-to": Number,
|
||
"-:": "--jump-to",
|
||
|
||
"--reverse": Boolean,
|
||
"-?": "--reverse",
|
||
},
|
||
{
|
||
argv: args,
|
||
permissive: true,
|
||
splitUnknownArguments: false,
|
||
},
|
||
)
|
||
const option = {}
|
||
option["reverse"] = Boolean(argOpt["--reverse"])
|
||
if ("--jump-to" in argOpt) option["jumpTo"] = argOpt["--jump-to"]
|
||
const searchQuery = argOpt._.join(" ")
|
||
return finding.jumpToMatch(searchQuery, option)
|
||
}
|
||
|
||
/** Jump to the next nth searched pattern.
|
||
*
|
||
* Available flags:
|
||
* - `-f` or `--search-from-view` to search from the current view instead of the previous match
|
||
* - `-?` or `--reverse` to reverse the sign of the number
|
||
*
|
||
* @param number - number of words to advance down the page (use 1 for next word, -1 for previous), default to 1
|
||
*
|
||
*/
|
||
//#content
|
||
export function findnext(...args: string[]) {
|
||
let n = 1
|
||
const option = arg.lib(
|
||
{
|
||
"--search-from-view": Boolean,
|
||
"-f": "--search-from-view",
|
||
|
||
"--reverse": Boolean,
|
||
"-?": "--reverse",
|
||
},
|
||
{
|
||
argv: args,
|
||
allowNegativePositional: true,
|
||
},
|
||
)
|
||
if (option._.length > 0) n = Number(option._[0])
|
||
if (option["--reverse"]) n = -n
|
||
return finding.jumpToNextMatch(n, Boolean(option["--search-from-view"]))
|
||
}
|
||
|
||
//#content
|
||
export function clearsearchhighlight() {
|
||
return finding.removeHighlighting()
|
||
}
|
||
|
||
/**
|
||
* Highlight the current find-mode match result and enter the visual mode.
|
||
*/
|
||
//#content
|
||
export function findselect() {
|
||
const range = finding.currentMatchRange()
|
||
const selection = document.getSelection()
|
||
selection.removeAllRanges()
|
||
selection.addRange(range)
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content_helper
|
||
function history(url_or_num: string, direction: number) {
|
||
url_or_num = url_or_num == "" ? "1" : url_or_num
|
||
isNaN(url_or_num as unknown as number) ? open(url_or_num) : window.history.go(parseInt(url_or_num, 10) * direction)
|
||
}
|
||
|
||
/** Navigate forward one page in history. */
|
||
//#content
|
||
export function forward(...args: string[]) {
|
||
return history(args.join(" "), 1)
|
||
}
|
||
|
||
/** Navigate back one page in history. */
|
||
//#content
|
||
export function back(...args: string[]) {
|
||
return history(args.join(" "), -1)
|
||
}
|
||
|
||
/** Reload the next n tabs, starting with activeTab, possibly bypassingCache */
|
||
//#background
|
||
export async function reload(n = 1, hard = false) {
|
||
const tabstoreload = await getnexttabs(await activeTabId(), n)
|
||
const reloadProperties = { bypassCache: hard }
|
||
return Promise.all(tabstoreload.map(n => browser.tabs.reload(n, reloadProperties)))
|
||
}
|
||
|
||
/** Reloads all tabs, bypassing the cache if hard is set to true */
|
||
//#background
|
||
export async function reloadall(hard = false) {
|
||
const tabs = await browser.tabs.query({ currentWindow: true })
|
||
const reloadprops = { bypassCache: hard }
|
||
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
|
||
* 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 })
|
||
const currId = await activeTabId()
|
||
tabs = tabs.filter(tab => tab.id !== currId)
|
||
const reloadprops = { bypassCache: hard }
|
||
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) {
|
||
return reload(n, true)
|
||
}
|
||
|
||
// I went through the whole list https://developer.mozilla.org/en-US/Firefox/The_about_protocol
|
||
// about:blank is even more special
|
||
/** @hidden */
|
||
export const ABOUT_WHITELIST = ["about:license", "about:logo", "about:rights", "about:blank"]
|
||
|
||
/**
|
||
* Open a new page in the current tab.
|
||
*
|
||
* @param urlarr
|
||
*
|
||
* - if first word looks like it has a schema, treat as a URI
|
||
* - else if the first word contains a dot, treat as a domain name
|
||
* - else if the first word is a key of [[SEARCH_URLS]], treat all following terms as search parameters for that provider
|
||
* - else treat as search parameters for [[searchengine]]
|
||
*
|
||
* Related settings: [[searchengine]], [[historyresults]]
|
||
*
|
||
* Can only open about:* or file:* URLs if you have the native messenger installed, and on OSX you must set `browser` to something that will open Firefox from a terminal pass it commmand line options.
|
||
*
|
||
*/
|
||
//#content
|
||
export async function open(...urlarr: string[]) {
|
||
const url = urlarr.join(" ")
|
||
|
||
// Setting window.location to about:blank results in a page we can't access, tabs.update works.
|
||
if (!ABOUT_WHITELIST.includes(url) && /^(about|file):.*/.exec(url)) {
|
||
// Open URLs that firefox won't let us by running `firefox <URL>` on the command line
|
||
return nativeopen(url)
|
||
} else if (/^javascript:/.exec(url)) {
|
||
const escapeUrl = url.replace(/[\\"]/g, "\\$&")
|
||
window.eval(`window.location.href = "${escapeUrl}"`)
|
||
} else {
|
||
const tab = await ownTab()
|
||
return openInTab(tab, {}, urlarr)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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" (e.g. `-c [container]` to open a bookmark in a container)
|
||
*/
|
||
//#background
|
||
export async function bmarks(opt: string, ...urlarr: string[]) {
|
||
if (opt === "-t") return tabopen(...urlarr)
|
||
else return open(opt, ...urlarr)
|
||
}
|
||
|
||
/**
|
||
* Like [[open]] but doesn't make a new entry in history.
|
||
*/
|
||
//#content
|
||
export async function open_quiet(...urlarr: string[]) {
|
||
const url = urlarr.join(" ")
|
||
|
||
if (!ABOUT_WHITELIST.includes(url) && /^(about|file):.*/.exec(url)) {
|
||
return nativeopen(url)
|
||
}
|
||
|
||
return ownTab().then(tab => openInTab(tab, { loadReplace: true }, urlarr))
|
||
}
|
||
|
||
/**
|
||
* If the url of the current document matches one of your search engines, will convert it to a list of arguments that open/tabopen will understand. If the url doesn't match any search engine, returns the url without modifications.
|
||
*
|
||
* For example, if you have searchurls.gi set to "https://www.google.com/search?q=%s&tbm=isch", using this function on a page you opened using "gi butterflies" will return "gi butterflies".
|
||
*
|
||
* This is useful when combined with fillcmdline, for example like this: `bind O composite url2args | fillcmdline open`.
|
||
*
|
||
* Note that this might break with search engines that redirect you to other pages/add GET parameters that do not exist in your searchurl.
|
||
*/
|
||
//#content
|
||
export async function url2args() {
|
||
const url = document.location.href
|
||
const searchurls = await config.getAsync("searchurls")
|
||
let result = url
|
||
|
||
for (const engine of Object.keys(searchurls)) {
|
||
const [beginning, end] = [...searchurls[engine].split("%s"), ""]
|
||
if (url.startsWith(beginning) && url.endsWith(end)) {
|
||
// Get the string matching %s
|
||
let encodedArgs = url.substring(beginning.length)
|
||
encodedArgs = encodedArgs.substring(0, encodedArgs.length - end.length)
|
||
// Remove any get parameters that might have been added by the search engine
|
||
// This works because if the user's query contains an "&", it will be encoded as %26
|
||
const amperpos = encodedArgs.search("&")
|
||
if (amperpos > 0) encodedArgs = encodedArgs.substring(0, amperpos)
|
||
|
||
// Do transformations depending on the search engine
|
||
if (beginning.search("duckduckgo") > 0) encodedArgs = encodedArgs.replace(/\+/g, " ")
|
||
else if (beginning.search("wikipedia") > 0) encodedArgs = encodedArgs.replace(/_/g, " ")
|
||
|
||
const args = engine + " " + decodeURIComponent(encodedArgs)
|
||
if (args.length < result.length) result = args
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content_helper
|
||
let sourceElement: Element
|
||
/** @hidden */
|
||
//#content_helper
|
||
function removeSource() {
|
||
if (sourceElement) {
|
||
sourceElement.remove()
|
||
sourceElement = undefined
|
||
}
|
||
}
|
||
|
||
/** Display the (HTML) source of the current page.
|
||
|
||
Behaviour can be changed by the 'viewsource' setting.
|
||
|
||
If the 'viewsource' setting is set to 'default' rather than 'tridactyl',
|
||
the url the source of which should be displayed can be given as argument.
|
||
Otherwise, the source of the current document will be displayed.
|
||
*/
|
||
//#content
|
||
export function viewsource(url = "") {
|
||
if (url === "") url = window.location.href
|
||
if (config.get("viewsource") === "default") {
|
||
window.location.href = "view-source:" + url
|
||
return
|
||
}
|
||
if (!sourceElement) {
|
||
sourceElement = CommandLineContent.executeWithoutCommandLine(() => {
|
||
const pre = document.createElement("pre")
|
||
pre.id = "TridactylViewsourceElement"
|
||
pre.className = "cleanslate " + config.get("theme")
|
||
pre.innerText = document.documentElement.innerHTML
|
||
document.documentElement.appendChild(pre)
|
||
window.addEventListener("popstate", removeSource)
|
||
return pre
|
||
})
|
||
} else {
|
||
sourceElement.parentNode.removeChild(sourceElement)
|
||
sourceElement = undefined
|
||
window.removeEventListener("popstate", removeSource)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Go to the homepages you have set with `set homepages ["url1", "url2"]`.
|
||
*
|
||
* @param all
|
||
* - if "true", opens all homepages in new tabs
|
||
* - if "false" or not given, opens the last homepage in the current tab
|
||
*
|
||
*/
|
||
//#background
|
||
export function home(all: "false" | "true" = "false") {
|
||
const homepages = config.get("homepages")
|
||
let done = Promise.resolve(undefined as any)
|
||
if (homepages.length > 0) {
|
||
if (all === "false") {
|
||
done = open(homepages[homepages.length - 1])
|
||
} else {
|
||
done = Promise.all(homepages.map(t => tabopen(t)))
|
||
}
|
||
}
|
||
return done.then(() => undefined)
|
||
}
|
||
|
||
/** Show this page.
|
||
|
||
`:help something` jumps to the entry for something. Something can be an excmd, an alias for an excmd, a binding or a setting.
|
||
|
||
On the ex command page, the "nmaps" list is a list of all the bindings for the command you're seeing and the "exaliases" list lists all its aliases.
|
||
|
||
If there's a conflict (e.g. you have a "go" binding that does something, a "go" excmd that does something else and a "go" setting that does a third thing), the binding is chosen first, then the setting, then the excmd. In such situations, if you want to let Tridactyl know you're looking for something specfic, you can specify the following flags as first arguments:
|
||
|
||
`-a`: look for an alias
|
||
`-b`: look for a binding
|
||
`-e`: look for an ex command
|
||
`-s`: look for a setting
|
||
|
||
If the keyword you gave to `:help` is actually an alias for a composite command (see [[composite]]) , you will be taken to the help section for the first command of the pipeline. You will be able to see the whole pipeline by hovering your mouse over the alias in the "exaliases" list. Unfortunately there currently is no way to display these HTML tooltips from the keyboard.
|
||
|
||
e.g. `:help bind`
|
||
*/
|
||
//#background
|
||
export async function help(...helpItems: string[]) {
|
||
const flags = {
|
||
// -a: look for an alias
|
||
"-a": (settings, helpItem) => {
|
||
const aliases = settings.exaliases
|
||
// As long as helpItem is an alias, try to resolve this alias to a real helpItem
|
||
const resolved = []
|
||
while (aliases[helpItem]) {
|
||
resolved.push(helpItem)
|
||
helpItem = aliases[helpItem].split(" ")
|
||
helpItem = helpItem[0] === "composite" ? helpItem[1] : helpItem[0]
|
||
// Prevent infinite loops
|
||
if (resolved.includes(helpItem)) break
|
||
}
|
||
if (resolved.length > 0) {
|
||
return browser.runtime.getURL("static/docs/modules/_src_excmds_.html") + "#" + helpItem
|
||
}
|
||
return ""
|
||
},
|
||
// -b: look for a binding
|
||
"-b": (settings, helpItem) => {
|
||
for (const mode of modeMaps) {
|
||
const bindings = settings[mode]
|
||
// If 'helpItem' matches a binding, replace 'helpItem' with
|
||
// the command that would be executed when pressing the key
|
||
// sequence referenced by 'helpItem' and don't check other
|
||
// modes
|
||
if (helpItem in bindings) {
|
||
helpItem = bindings[helpItem].split(" ")
|
||
helpItem = ["composite", "fillcmdline"].includes(helpItem[0]) ? helpItem[1] : helpItem[0]
|
||
return browser.runtime.getURL("static/docs/modules/_src_excmds_.html") + "#" + helpItem
|
||
}
|
||
}
|
||
return ""
|
||
},
|
||
// -e: look for an excmd
|
||
"-e": (settings, helpItem) => browser.runtime.getURL("static/docs/modules/_src_excmds_.html") + "#" + helpItem,
|
||
// -s: look for a setting
|
||
"-s": (settings, helpItem) => {
|
||
let subSettings = settings
|
||
const settingNames = helpItem.split(".")
|
||
let settingHelpAnchor = ""
|
||
// Try to match each piece of the path, this is done so that a correct object (e.g. followpagepatterns) with an incorrect key (e.g. nextt) will still match the parent object.
|
||
for (const settingName of settingNames) {
|
||
if (settingName in subSettings) {
|
||
settingHelpAnchor += settingName + "."
|
||
subSettings = subSettings[settingName]
|
||
}
|
||
}
|
||
if (settingHelpAnchor !== "") {
|
||
return browser.runtime.getURL("static/docs/classes/_src_lib_config_.default_config.html") + "#" + settingHelpAnchor.slice(0, -1)
|
||
}
|
||
return ""
|
||
},
|
||
}
|
||
|
||
let flag = ""
|
||
|
||
if (helpItems.length > 0 && Object.keys(flags).includes(helpItems[0])) {
|
||
flag = helpItems[0]
|
||
helpItems.splice(0, 1)
|
||
}
|
||
|
||
const subject = helpItems.join(" ")
|
||
const settings = await config.getAsync()
|
||
let url = ""
|
||
if (subject === "") {
|
||
url = browser.runtime.getURL("static/docs/modules/_src_excmds_.html")
|
||
} else {
|
||
// If the user did specify what they wanted, specifically look for it
|
||
if (flag !== "") {
|
||
url = flags[flag](settings, subject)
|
||
}
|
||
|
||
// Otherwise or if it couldn't be found, try all possible items
|
||
if (url === "") {
|
||
url = ["-b", "-s", "-a", "-e"].reduce((acc, curFlag) => {
|
||
if (acc !== "") return acc
|
||
return flags[curFlag](settings, subject)
|
||
}, "")
|
||
}
|
||
}
|
||
|
||
let done
|
||
if ((await activeTab()).url.startsWith(browser.runtime.getURL("static/docs/"))) {
|
||
done = open(url)
|
||
} else {
|
||
done = tabopen(url)
|
||
}
|
||
return done.then(() => undefined)
|
||
}
|
||
|
||
/**
|
||
* Search through the help pages. Accepts the same flags as [[help]]. Only useful in interactive usage with completions; the command itself is just a wrapper for [[help]].
|
||
*/
|
||
//#background
|
||
export async function apropos(...helpItems: string[]) {
|
||
return help(...helpItems)
|
||
}
|
||
|
||
/** Start the tutorial
|
||
* @param newtab - whether to start the tutorial in a newtab. Defaults to current tab.
|
||
*/
|
||
//#background
|
||
export async function tutor(newtab?: string) {
|
||
const tutor = browser.runtime.getURL("static/clippy/1-tutor.html")
|
||
let done: Promise<any>
|
||
if (newtab) {
|
||
done = tabopen(tutor)
|
||
} else {
|
||
done = open(tutor)
|
||
}
|
||
return done.then(() => undefined)
|
||
}
|
||
|
||
/**
|
||
* Display Tridactyl's contributors in order of commits in a user-friendly fashion
|
||
*/
|
||
//#background
|
||
export async function credits() {
|
||
const creditspage = browser.runtime.getURL("static/authors.html")
|
||
return tabopen(creditspage)
|
||
}
|
||
|
||
/**
|
||
* 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`
|
||
*
|
||
* "There is no mouse".
|
||
*/
|
||
//#content
|
||
export function no_mouse_mode() {
|
||
toys.no_mouse()
|
||
}
|
||
|
||
/**
|
||
* Matrix variant of [[no_mouse_mode]]
|
||
*
|
||
* "There is no mouse".
|
||
*
|
||
* Coincidentally added to Tridactyl at the same time as we reached 1337 stars on GitHub.
|
||
*/
|
||
//#content
|
||
export function neo_mouse_mode() {
|
||
toys.jack_in()
|
||
}
|
||
|
||
/**
|
||
* Christmas variant of [[no_mouse_mode]] (if you live in $DEFAULT hemisphere).
|
||
*/
|
||
//#content
|
||
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 <C-\> 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.
|
||
//
|
||
// If no matching link is found, return undefined.
|
||
//
|
||
// We return the last link that matches because next/prev buttons tend to be at the end of the page
|
||
// whereas lots of blogs have "VIEW MORE" etc. plastered all over their pages.
|
||
//#content_helper
|
||
function findRelLink(pattern: RegExp): HTMLAnchorElement | null {
|
||
// querySelectorAll returns a "non-live NodeList" which is just a shit array without working reverse() or find() calls, so convert it.
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||
const links = Array.from(document.querySelectorAll("a[href]") as NodeListOf<HTMLAnchorElement>)
|
||
|
||
// Find the last link that matches the test
|
||
return links.reverse().find(link => pattern.test(link.innerText))
|
||
|
||
// Note:
|
||
// `innerText` gives better (i.e. less surprising) results than `textContent`
|
||
// at the expense of being much slower, but that shouldn't be an issue here
|
||
// as it's a one-off operation that's only performed when we're leaving a page
|
||
}
|
||
|
||
/** @hidden */
|
||
// Return the last element in the document matching the supplied selector,
|
||
// or null if there are no matches.
|
||
function selectLast(selector: string): HTMLElement | null {
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||
const nodes = document.querySelectorAll(selector) as NodeListOf<HTMLElement>
|
||
return nodes.length ? nodes[nodes.length - 1] : null
|
||
}
|
||
|
||
/** Find a likely next/previous link and follow it
|
||
|
||
If a link or anchor element with rel=rel exists, use that, otherwise fall back to:
|
||
|
||
1) find the last anchor on the page with innerText matching the appropriate `followpagepattern`.
|
||
2) call [[urlincrement]] with 1 or -1
|
||
|
||
If you want to support e.g. French:
|
||
|
||
```
|
||
set followpagepatterns.next ^(next|newer|prochain)\b|»|>>
|
||
set followpagepatterns.prev ^(prev(ious)?|older|précédent)\b|«|<<
|
||
```
|
||
|
||
@param rel the relation of the target page to the current page: "next" or "prev"
|
||
*/
|
||
//#content
|
||
export function followpage(rel: "next" | "prev" = "next") {
|
||
const link = selectLast(`link[rel~=${rel}][href]`) as HTMLLinkElement
|
||
|
||
if (link) {
|
||
window.location.href = link.href
|
||
return
|
||
}
|
||
|
||
const anchor = (selectLast(`a[rel~=${rel}][href]`) || findRelLink(new RegExp(config.get("followpagepatterns", rel), "i"))) as HTMLAnchorElement
|
||
|
||
if (anchor) {
|
||
DOM.mouseEvent(anchor, "click")
|
||
} else {
|
||
urlincrement(rel === "next" ? 1 : -1)
|
||
}
|
||
}
|
||
|
||
/** 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<C-x>` works.
|
||
*/
|
||
//#content
|
||
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
|
||
try {
|
||
window.location.href = newUrl
|
||
} catch (e) {
|
||
logger.info(`urlincrement: Impossible to navigate to ${newUrl}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Go to the root domain of the current URL
|
||
*/
|
||
//#content
|
||
export function urlroot() {
|
||
const rootUrl = UrlUtil.getUrlRoot(window.location)
|
||
|
||
if (rootUrl !== null) {
|
||
window.location.href = rootUrl.href
|
||
}
|
||
}
|
||
|
||
/** Go to the parent URL of the current tab's URL
|
||
*/
|
||
//#content
|
||
export function urlparent(count = 1) {
|
||
const option = {}
|
||
for (const key of "trailingSlash ignoreFragment ignoreSearch".split(" ")) {
|
||
const configKey = ("urlparent" + key.toLowerCase()) as keyof config.default_config
|
||
option[key] = config.get(configKey) === "true"
|
||
}
|
||
const regexpString = config.get("urlparentignorepathregexp")
|
||
const regexpScan = regexpString.match(/^\/(.+)\/([a-z]*?)$/)
|
||
if (regexpString && regexpScan) {
|
||
option["ignorePathRegExp"] = new RegExp(regexpScan[1], regexpScan[2])
|
||
}
|
||
|
||
const parentUrl = UrlUtil.getUrlParent(window.location, option, count)
|
||
|
||
if (parentUrl !== null) {
|
||
window.location.href = parentUrl.href
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Open a URL made by modifying the current URL
|
||
*
|
||
* There are several modes:
|
||
*
|
||
* * Text replace mode: `urlmodify -t <old> <new>`
|
||
*
|
||
* Replaces the first instance of the text `old` with `new`.
|
||
* * `http://example.com` -> (`-t exa peta`) -> `http://petample.com`
|
||
*
|
||
* * Regex replacment mode: `urlmodify -r <regexp> <new> [flags]`
|
||
*
|
||
* Replaces the first match of the `regexp` with `new`. You can use
|
||
* flags `i` and `g` to match case-insensitively and to match
|
||
* all instances respectively
|
||
* * `http://example.com` -> (`-r [ea] X g`) -> `http://XxXmplX.com`
|
||
*
|
||
* * Query set mode: `urlmodify -s <query> <value>`
|
||
*
|
||
* Sets the value of a query to be a specific one. If the query already
|
||
* exists, it will be replaced.
|
||
* * `http://e.com?id=abc` -> (`-s foo bar`) -> `http://e.com?id=abc&foo=bar
|
||
*
|
||
* * Query replace mode: `urlmodify -q <query> <new_val>`
|
||
*
|
||
* Replace the value of a query with a new one:
|
||
* * `http://e.com?id=foo` -> (`-q id bar`) -> `http://e.com?id=bar
|
||
*
|
||
* * Query delete mode: `urlmodify -Q <query>`
|
||
*
|
||
* Deletes the given query (and the value if any):
|
||
* * `http://e.com?id=foo&page=1` -> (`-Q id`) -> `http://e.com?page=1`
|
||
*
|
||
* * Graft mode: `urlmodify -g <graft_point> <new_path_tail>`
|
||
*
|
||
* "Grafts" a new tail on the URL path, possibly removing some of the old
|
||
* tail. Graft point indicates where the old URL is truncated before adding
|
||
* the new path.
|
||
*
|
||
* * `graft_point` >= 0 counts path levels, starting from the left
|
||
* (beginning). 0 will append from the "root", and no existing path will
|
||
* remain, 1 will keep one path level, and so on.
|
||
* * `graft_point` < 0 counts from the right (i.e. the end of the current
|
||
* path). -1 will append to the existing path, -2 will remove the last path
|
||
* level, and so on.
|
||
*
|
||
* ```plaintext
|
||
* http://website.com/this/is/the/path/component
|
||
* Graft point: ^ ^ ^ ^ ^ ^
|
||
* From left: 0 1 2 3 4 5
|
||
* From right: -6 -5 -4 -3 -2 -1
|
||
* ```
|
||
*
|
||
* Examples:
|
||
*
|
||
* * `http://e.com/issues/42` -> (`-g 0 foo`) -> `http://e.com/foo`
|
||
* * `http://e.com/issues/42` -> (`-g 1 foo`) -> `http://e.com/issues/foo`
|
||
* * `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 <arguments> <URL>`
|
||
*
|
||
* Each mode can be augmented to accept a URL as the last argument instead of
|
||
* the current url.
|
||
*
|
||
* Examples:
|
||
*
|
||
* * `urlmodify -tu <old> <new> <URL>`
|
||
* * `urlmodify -su <query> <value> <URL>`
|
||
* * `urlmodify -gu <graft_point> <new_path_tail> <URL>`
|
||
*
|
||
* @param mode The replace mode:
|
||
* * -t text replace
|
||
* * -r regexp replace
|
||
* * -s set the value of the given query
|
||
* * -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 <old> <new>
|
||
* * -r <regexp> <new> [flags]
|
||
* * -s <query> <value>
|
||
* * -q <query> <new_val>
|
||
* * -Q <query>
|
||
* * -g <graftPoint> <newPathTail>
|
||
* * -*u <arguments> <URL>
|
||
*/
|
||
//#content
|
||
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) {
|
||
window.location.replace(newUrl)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Like [[urlmodify]] but returns the modified URL for use with [[js]] and [[composite]]
|
||
*
|
||
* E.g.
|
||
*
|
||
* `:composite urlmodify_js -t www. old. | tabopen `
|
||
*/
|
||
//#content
|
||
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 (newmode) {
|
||
case "-t":
|
||
if (args.length !== 2) {
|
||
throw new Error("Text replacement needs 2 arguments:" + "<old> <new>")
|
||
}
|
||
|
||
newUrl = oldUrl.href.replace(args[0], args[1])
|
||
break
|
||
|
||
case "-r":
|
||
if (args.length < 2 || args.length > 3) {
|
||
throw new Error("RegExp replacement takes 2 or 3 arguments: " + "<regexp> <new> [flags]")
|
||
}
|
||
|
||
if (args[2] && args[2].search(/^[gi]+$/) === -1) {
|
||
throw new Error("RegExp replacement flags can only include 'g', 'i'" + ", Got '" + args[2] + "'")
|
||
}
|
||
|
||
const regexp = new RegExp(args[0], args[2])
|
||
newUrl = oldUrl.href.replace(regexp, args[1])
|
||
break
|
||
|
||
case "-s":
|
||
if (args.length !== 2) {
|
||
throw new Error("Query setting needs 2 arguments:" + "<query> <value>")
|
||
}
|
||
|
||
newUrl = UrlUtil.setQueryValue(oldUrl, args[0], args[1])
|
||
break
|
||
case "-q":
|
||
if (args.length !== 2) {
|
||
throw new Error("Query replacement needs 2 arguments:" + "<query> <new_val>")
|
||
}
|
||
|
||
newUrl = UrlUtil.replaceQueryValue(oldUrl, args[0], args[1])
|
||
break
|
||
case "-Q":
|
||
if (args.length !== 1) {
|
||
throw new Error("Query deletion needs 1 argument:" + "<query>")
|
||
}
|
||
|
||
newUrl = UrlUtil.deleteQuery(oldUrl, args[0])
|
||
break
|
||
|
||
case "-g":
|
||
if (args.length !== 2) {
|
||
throw new Error("URL path grafting needs 2 arguments:" + "<graft point> <new path tail>")
|
||
}
|
||
|
||
newUrl = UrlUtil.graftUrlPath(oldUrl, args[1], Number(args[0]))
|
||
break
|
||
}
|
||
|
||
return newUrl
|
||
}
|
||
|
||
/** Returns the url of links that have a matching rel.
|
||
|
||
Don't bind to this: it's an internal function.
|
||
|
||
@hidden
|
||
*/
|
||
//#content
|
||
export async function geturlsforlinks(reltype = "rel", rel: string) {
|
||
const elems = document.querySelectorAll("link[" + reltype + "='" + rel + "']")
|
||
if (elems) return Array.prototype.map.call(elems, x => x.href)
|
||
return []
|
||
}
|
||
|
||
/** 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. 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. 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", 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}`)
|
||
}
|
||
if (rel === "true") {
|
||
level += await browser.tabs.getZoom()
|
||
|
||
// Handle overshooting of zoom level.
|
||
if (level > 3) level = 3
|
||
if (level < 0.3) level = 0.3
|
||
}
|
||
|
||
if (tabId === "auto") {
|
||
return browser.tabs.setZoom(level)
|
||
} else {
|
||
return browser.tabs.setZoom(parseInt(tabId, 10), level)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @hidden
|
||
* Old version of the reader command. Opens the current page in Firefox's reader mode.
|
||
* You cannot use Tridactyl while in this reader mode.
|
||
*
|
||
* Use [[reader]] instead
|
||
*/
|
||
//#background
|
||
export async function readerold() {
|
||
if (await firefoxVersionAtLeast(58)) {
|
||
const aTab = await activeTab()
|
||
if (aTab.isArticle) {
|
||
return browser.tabs.toggleReaderMode()
|
||
} // else {
|
||
// // once a statusbar exists an error can be displayed there
|
||
// }
|
||
}
|
||
}
|
||
|
||
/** @hidden **/
|
||
//#content_helper
|
||
// {
|
||
loadaucmds("DocStart")
|
||
const autocmd_logger = new Logging.Logger("autocmds")
|
||
window.addEventListener("pagehide", () => loadaucmds("DocEnd"))
|
||
window.addEventListener("DOMContentLoaded", () => {
|
||
loadaucmds("DocLoad")
|
||
})
|
||
window.addEventListener("HistoryState", () => loadaucmds("HistoryState"))
|
||
|
||
// 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")
|
||
if (document.fullscreenElement || (document as any).mozFullScreenElement) {
|
||
loadaucmds("FullscreenEnter")
|
||
} else {
|
||
loadaucmds("FullscreenLeft")
|
||
}
|
||
}
|
||
|
||
/** @hidden **/
|
||
const fullscreenApiIsPrefixed = "mozFullScreenEnabled" in document
|
||
|
||
// Until firefox removes vendor prefix for this api (in FF64), we must also use mozfullscreenchange
|
||
if (fullscreenApiIsPrefixed) {
|
||
document.addEventListener("mozfullscreenchange", fullscreenhandler)
|
||
} else if ("fullscreenEnabled" in document) {
|
||
document.addEventListener("fullscreenchange", fullscreenhandler)
|
||
}
|
||
// }
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export async function loadaucmds(cmdType: "DocStart" | "DocLoad" | "DocEnd" | "TabEnter" | "TabLeft" | "FullscreenEnter" | "FullscreenLeft" | "FullscreenChange" | "UriChange" | "HistoryState") {
|
||
const aucmds = await config.getAsync("autocmds", cmdType)
|
||
if (!aucmds) return
|
||
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) {
|
||
autocmd_logger.error((e as Error).toString())
|
||
}
|
||
}
|
||
}
|
||
|
||
/** The kinds of input elements that we want to be included in the "focusinput"
|
||
* command (gi)
|
||
* @hidden
|
||
*/
|
||
export const INPUTTAGS_selectors = `
|
||
input:not([disabled]):not([readonly]):-moz-any(
|
||
:not([type]),
|
||
[type='text'],
|
||
[type='search'],
|
||
[type='password'],
|
||
[type='datetime'],
|
||
[type='datetime-local'],
|
||
[type='date'],
|
||
[type='month'],
|
||
[type='time'],
|
||
[type='week'],
|
||
[type='number'],
|
||
[type='range'],
|
||
[type='email'],
|
||
[type='url'],
|
||
[type='tel'],
|
||
[type='color']
|
||
),
|
||
textarea:not([disabled]):not([readonly]),
|
||
object,
|
||
[role='application'],
|
||
[contenteditable='true'][role='textbox']
|
||
`
|
||
|
||
/** Password field selectors
|
||
* @hidden
|
||
*/
|
||
const INPUTPASSWORD_selectors = `
|
||
input[type='password']
|
||
`
|
||
|
||
/** Focus the last used input on the page
|
||
*
|
||
* @param nth focus the nth input on the page, or "special" inputs:
|
||
* "-l": last focussed input
|
||
* "-n": input after last focussed one
|
||
* "-N": input before last focussed one
|
||
* "-p": first password field
|
||
* "-b": biggest input field
|
||
*/
|
||
//#content
|
||
export function focusinput(nth: number | string) {
|
||
let inputToFocus: HTMLElement = null
|
||
|
||
// set to false to avoid falling back on the first available input
|
||
// if a special finder fails
|
||
let fallbackToNumeric = true
|
||
|
||
// nth = "-l" -> use the last used input for this page
|
||
if (nth === "-l") {
|
||
// try to recover the last used input stored as a
|
||
// DOM node, which should be exactly the one used before (or null)
|
||
if (DOM.getLastUsedInput()) {
|
||
inputToFocus = DOM.getLastUsedInput()
|
||
} else {
|
||
// Pick the first input in the DOM.
|
||
inputToFocus = DOM.getElemsBySelector(INPUTTAGS_selectors, [DOM.isSubstantial])[0] as HTMLElement
|
||
|
||
// We could try to save the last used element on page exit, but
|
||
// that seems like a lot of faff for little gain.
|
||
}
|
||
} else if (nth === "-n" || nth === "-N") {
|
||
// attempt to find next/previous input
|
||
const inputs = DOM.getElemsBySelector(INPUTTAGS_selectors, [DOM.isSubstantial]) as HTMLElement[]
|
||
if (inputs.length) {
|
||
let index = inputs.indexOf(DOM.getLastUsedInput())
|
||
if (DOM.getLastUsedInput()) {
|
||
if (nth === "-n") {
|
||
index++
|
||
} else {
|
||
index--
|
||
}
|
||
index = index.mod(inputs.length)
|
||
} else {
|
||
index = 0
|
||
}
|
||
inputToFocus = inputs[index]
|
||
}
|
||
} else if (nth === "-p") {
|
||
// attempt to find a password input
|
||
fallbackToNumeric = false
|
||
|
||
const inputs = DOM.getElemsBySelector(INPUTPASSWORD_selectors, [DOM.isSubstantial])
|
||
|
||
if (inputs.length) {
|
||
inputToFocus = inputs[0] as HTMLElement
|
||
}
|
||
} else if (nth === "-b") {
|
||
const inputs = DOM.getElemsBySelector(INPUTTAGS_selectors, [DOM.isSubstantial]) as HTMLElement[]
|
||
inputs.sort(DOM.compareElementArea)
|
||
inputToFocus = inputs[inputs.length - 1]
|
||
}
|
||
|
||
// either a number (not special) or we failed to find a special input when
|
||
// asked and falling back is acceptable
|
||
if ((!inputToFocus || !document.contains(inputToFocus)) && fallbackToNumeric) {
|
||
const index = isNaN(nth as number) ? 0 : (nth as number)
|
||
inputToFocus = DOM.getNthElement(INPUTTAGS_selectors, index, [DOM.isSubstantial])
|
||
}
|
||
|
||
if (inputToFocus) {
|
||
DOM.focus(inputToFocus)
|
||
if (config.get("gimode") === "nextinput" && contentState.mode !== "input") {
|
||
contentState.mode = "input"
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Focus the tab which contains the last focussed input element. If you're lucky, it will focus the right input, too.
|
||
*
|
||
* Currently just goes to the last focussed input; being able to jump forwards and backwards is planned.
|
||
*/
|
||
//#background
|
||
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]
|
||
await browser.tabs.update(input.tab, { active: true })
|
||
const id = input.inputId
|
||
// Not all elements have an ID, so this will do for now.
|
||
if (id) focusbyid(input.inputId)
|
||
else focusinput("-l")
|
||
|
||
// Really want to bin the input we just focussed ^ and edit the real last input to tell us where to jump to next.
|
||
// It doesn't work in practice as the focus events get added after we try to delete them.
|
||
// Even editing focusbyid/focusinput doesn't work to try to delete their own history doesn't work.
|
||
// I'm bored of working on it for now, though.
|
||
// Probable solution: add an event listener to state.prevInputs changing, delete the focussed element, then delete event listener.
|
||
//
|
||
// let arr = state.prevInputs
|
||
// arr.splice(-2,2)
|
||
|
||
// tail.jumppos = jumppos - 1
|
||
// arr = arr.concat(tail)
|
||
// state.prevInputs = arr
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export function focusbyid(id: string) {
|
||
document.getElementById(id).focus()
|
||
}
|
||
|
||
// }}}
|
||
|
||
// {{{ TABS
|
||
|
||
/** Switch to the tab by index (position on tab bar), wrapping round.
|
||
|
||
@param index
|
||
1-based index of the tab to target. Wraps such that 0 = last tab, -1 =
|
||
penultimate tab, etc.
|
||
|
||
if undefined, return activeTabId()
|
||
*/
|
||
/** @hidden */
|
||
//#background_helper
|
||
async function tabIndexSetActive(index: number | string) {
|
||
return tabSetActive(await idFromIndex(index))
|
||
}
|
||
|
||
/** Switch to the next tab, wrapping round.
|
||
|
||
If increment is specified, move that many tabs forwards.
|
||
*/
|
||
//#background
|
||
export async function tabnext(increment = 1) {
|
||
return tabprev(-increment)
|
||
}
|
||
|
||
/** Switch to the next tab, wrapping round.
|
||
|
||
If an index is specified, go to the tab with that number (this mimics the
|
||
behaviour of `{count}gt` in vim, except that this command will accept a
|
||
count that is out of bounds (and will mod it so that it is within bounds as
|
||
per [[tabmove]], etc)).
|
||
*/
|
||
//#background
|
||
export async function tabnext_gt(index?: number) {
|
||
let done: Promise<any>
|
||
if (index === undefined) {
|
||
done = tabnext()
|
||
} else {
|
||
done = tabIndexSetActive(index)
|
||
}
|
||
return done.then(() => undefined)
|
||
}
|
||
|
||
/** Switch to the previous tab, wrapping round.
|
||
|
||
If increment is specified, move that many tabs backwards.
|
||
*/
|
||
//#background
|
||
export async function tabprev(increment = 1) {
|
||
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
|
||
return browser.tabs.update(tabs[prevTab].id, { active: true })
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 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) {
|
||
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()
|
||
const winId = windowId ?? nextWindow.id
|
||
const pos = await config.getAsync("tabopenpos")
|
||
if (pos == "last") {
|
||
return browser.tabs.move(tabId, { index: -1, windowId: winId })
|
||
} else {
|
||
const index = (await activeTabOnWindow(winId)).index + 1
|
||
return browser.tabs.move(tabId, { index, windowId: winId })
|
||
}
|
||
}
|
||
|
||
/** 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 })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Moves all of the targetted window's tabs to the current window. Only works for windows of the same type
|
||
* (can't merge a non-private tab with a private window).
|
||
*/
|
||
//#background
|
||
export async function winmerge(...windowIds: string[]) {
|
||
const target_wins = windowIds.length > 0 ? await Promise.all(windowIds.map(windowId => browser.windows.get(parseInt(windowId, 10), { populate: true }))) : await browser.windows.getAll({ populate: true })
|
||
const active_win = await browser.windows.getCurrent()
|
||
return target_wins.forEach(target_win =>
|
||
browser.tabs.move(
|
||
target_win.tabs.map(t => t.id),
|
||
{ index: -1, windowId: active_win.id },
|
||
),
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Given a string of the format windowIndex.tabIndex, returns a tuple of
|
||
* numbers corresponding to the window index and tab index or the current
|
||
* window and tab if the string doesn't have the right format.
|
||
*/
|
||
//#background_helper
|
||
async function parseWinTabIndex(id: string) {
|
||
const windows = (await browser.windows.getAll()).map(w => w.id).sort((a, b) => a - b)
|
||
if (id === null || id === undefined || !/\d+\.\d+/.exec(id)) {
|
||
const tab = await activeTab()
|
||
const prevId = id
|
||
id = windows.indexOf(tab.windowId) + "." + (tab.index + 1)
|
||
logger.info(`taball: Bad tab id: ${prevId}, defaulting to ${id}`)
|
||
}
|
||
const [winindex, tabindex_string] = id.split(".")
|
||
return [windows[parseInt(winindex, 10) - 1], parseInt(tabindex_string, 10) - 1]
|
||
}
|
||
|
||
/**
|
||
* Moves a tab identified by a windowIndex.tabIndex id to the current window.
|
||
* Only works for windows of the same type (can't grab a non-private tab from a
|
||
* private window and can't grab a private tab from a non-private window).
|
||
*/
|
||
//#background
|
||
export async function tabgrab(id: string) {
|
||
// Figure out what tab should be grabbed
|
||
const [winid, tabindex_number] = await parseWinTabIndex(id)
|
||
const tabid = (await browser.tabs.query({ windowId: winid, index: tabindex_number }))[0].id
|
||
// Figure out where it should be put
|
||
const windowId = (await browser.windows.getLastFocused({ windowTypes: ["normal"] })).id
|
||
// Move window
|
||
const pos = await config.getAsync("tabopenpos")
|
||
if (pos == "last") {
|
||
return browser.tabs.move(tabid, { index: -1, windowId })
|
||
} else {
|
||
const index = (await activeTab()).index + 1
|
||
return browser.tabs.move(tabid, { index, windowId })
|
||
}
|
||
}
|
||
|
||
/** 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.
|
||
|
||
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`.
|
||
|
||
The special flag "--focus-address-bar" should focus the Firefox address bar after opening if no URL is provided.
|
||
|
||
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
|
||
the authors' preference.
|
||
|
||
If you would rather the Firefox behaviour `set tabopenpos last`. This
|
||
preference also affects the clipboard, quickmarks, home, help, etc.
|
||
|
||
If you would rather the URL be opened as if you'd middle clicked it, `set
|
||
tabopenpos related`.
|
||
|
||
Hinting is controlled by `relatedopenpos`
|
||
|
||
Also see the [[searchengine]] and [[searchurls]] settings.
|
||
*/
|
||
//#background
|
||
export async function tabopen(...addressarr: string[]): Promise<browser.tabs.Tab> {
|
||
return tabopen_helper({ addressarr })
|
||
}
|
||
|
||
/**
|
||
* Like [[tabopen]] but waits for the DOM to load before resolving its promise. Useful if you're hoping to execute ex-commands in that tab.
|
||
*/
|
||
//#background
|
||
export async function tabopenwait(...addressarr: string[]): Promise<browser.tabs.Tab> {
|
||
return tabopen_helper({ addressarr, waitForDom: true })
|
||
}
|
||
|
||
/**
|
||
* @hidden
|
||
*/
|
||
//#background_helper
|
||
export async function tabopen_helper({ addressarr = [], waitForDom = false }): Promise<browser.tabs.Tab> {
|
||
let active
|
||
let container
|
||
let bypassFocusHack = false
|
||
let discarded = false
|
||
|
||
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: string[]): Promise<string[]> {
|
||
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] === "--focus-address-bar") {
|
||
bypassFocusHack = true
|
||
args.shift()
|
||
argParse(args)
|
||
} else if (args[0] === "--discard") {
|
||
discarded = true
|
||
active = false
|
||
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") {
|
||
container = "firefox-default"
|
||
} else {
|
||
container = await Container.fuzzyMatch(args[1])
|
||
}
|
||
} else logger.error("[tabopen] can't open a container in a private browsing window.")
|
||
|
||
args.shift()
|
||
args.shift()
|
||
argParse(args)
|
||
}
|
||
return args
|
||
}
|
||
|
||
const query = await argParse(addressarr)
|
||
|
||
const address = query.join(" ")
|
||
if (!ABOUT_WHITELIST.includes(address) && /^(about|file):.*/.exec(address)) {
|
||
return nativeopen(address) as unknown as browser.tabs.Tab // I don't understand why changing the final return below meant I had to change this
|
||
}
|
||
|
||
const aucon = new AutoContain()
|
||
if (!container && aucon.autocontainConfigured()) {
|
||
const [autoContainer] = await aucon.getAuconAndProxiesForUrl(address)
|
||
if (autoContainer && autoContainer !== "firefox-default") {
|
||
container = autoContainer
|
||
logger.debug("tabopen setting container automatically using autocontain directive")
|
||
}
|
||
}
|
||
|
||
const containerId = await activeTabContainerId()
|
||
const args = { active } as any
|
||
// Ensure -c has priority.
|
||
if (container) {
|
||
if (container !== "firefox-default") {
|
||
args.cookieStoreId = container
|
||
}
|
||
} else if (containerId && config.get("tabopencontaineraware") === "true") {
|
||
args.cookieStoreId = containerId
|
||
}
|
||
args.bypassFocusHack = bypassFocusHack
|
||
args.discarded = discarded
|
||
const maybeURL = await queryAndURLwrangler(query)
|
||
if (typeof maybeURL === "string") {
|
||
return openInNewTab(maybeURL, args, waitForDom)
|
||
}
|
||
|
||
if (typeof maybeURL === "object") {
|
||
// browser.search.search(tabId, ...) sometimes does not work when it is executed
|
||
// right after openInNewTab(). Calling browser.tabs.get() between openInNewTab()
|
||
// and browser.search.search() seems to fix that problem.
|
||
// See https://github.com/tridactyl/tridactyl/pull/4791.
|
||
return openInNewTab(null, args, waitForDom)
|
||
.then(tab => browser.tabs.get(tab.id))
|
||
.then(tab => browser.search.search({tabId: tab.id, ...maybeURL}))
|
||
}
|
||
|
||
// Fall back to about:newtab
|
||
return openInNewTab(null, args, waitForDom)
|
||
}
|
||
|
||
/**
|
||
Passes its first argument to `tabopen -b`. Once the tab opened by `tabopen
|
||
-b` is activated/selected/focused, opens its second argument with `tabopen
|
||
-b`. Once the second tab is activated/selected/focused, opens its third
|
||
argument with `tabopen -b` and so on and so forth until all arguments have
|
||
been opened in a new tab or until a tab is closed without being
|
||
activated/selected/focused.
|
||
|
||
Example usage:
|
||
`tabqueue http://example.org http://example.com http://example.net`
|
||
`composite hint -qpipe a href | tabqueue`
|
||
*/
|
||
//#background
|
||
export function tabqueue(...addresses: string[]) {
|
||
// addresses[0] is a string when called with `tabopen a b c` but an array
|
||
// when called from `composite hint -qpipe a href | tabqueue`.
|
||
addresses = addresses.flat(Infinity)
|
||
if (addresses.length === 0) {
|
||
return Promise.resolve()
|
||
}
|
||
return tabopen("-b", addresses[0]).then(
|
||
tab =>
|
||
new Promise(resolve => {
|
||
function openNextTab(activeInfo) {
|
||
if (activeInfo.tabId === tab.id) {
|
||
resolve(tabqueue(...addresses.slice(1)))
|
||
removeTabqueueListeners(tab.id)
|
||
}
|
||
}
|
||
function removeTabqueueListeners(tabId) {
|
||
if (tabId === tab.id) {
|
||
browser.tabs.onActivated.removeListener(openNextTab)
|
||
browser.tabs.onRemoved.removeListener(removeTabqueueListeners)
|
||
// FIXME: This should actually be `reject(tab)` to
|
||
// interrupt pipelines, but this results in an impossible
|
||
// to debug `Error: undefined` message being printed on the
|
||
// command line. So we silently resolve the promise and
|
||
// hope for the best.
|
||
resolve(tab)
|
||
}
|
||
}
|
||
browser.tabs.onActivated.addListener(openNextTab)
|
||
browser.tabs.onRemoved.addListener(removeTabqueueListeners)
|
||
}),
|
||
)
|
||
}
|
||
|
||
/** Resolve a tab index to the tab id of the corresponding tab in this window.
|
||
|
||
@param index
|
||
1-based index of the tab to target. Wraps such that 0 = last tab, -1 =
|
||
penultimate tab, etc.
|
||
|
||
also supports # for previous tab, % for current tab.
|
||
|
||
if undefined, return activeTabId()
|
||
|
||
@hidden
|
||
*/
|
||
//#background_helper
|
||
async function idFromIndex(index?: number | "%" | "#" | string): Promise<number> {
|
||
return (await tabFromIndex(index)).id
|
||
}
|
||
|
||
/**
|
||
* Like [[idFromIndex]] but returns the whole tab object
|
||
*
|
||
* @hidden
|
||
*/
|
||
//#background_helper
|
||
async function tabFromIndex(index?: number | "%" | "#" | string): Promise<browser.tabs.Tab> {
|
||
if (index === "#") {
|
||
// Support magic previous/current tab syntax everywhere
|
||
return prevActiveTab()
|
||
} else if (index !== undefined && index !== "%") {
|
||
const tabs = await getSortedTabs()
|
||
index = Number(index)
|
||
index = (index - 1).mod(tabs.length) + 1
|
||
|
||
return tabs[index - 1]
|
||
} else {
|
||
return activeTab()
|
||
}
|
||
}
|
||
|
||
/** Close all other tabs in this window */
|
||
//#background
|
||
export async function tabonly() {
|
||
const tabs = await browser.tabs.query({
|
||
pinned: false,
|
||
active: false,
|
||
currentWindow: true,
|
||
})
|
||
const tabsIds = tabs.map(tab => tab.id)
|
||
return browser.tabs.remove(tabsIds)
|
||
}
|
||
|
||
/** Duplicate a tab.
|
||
|
||
@param index
|
||
The 1-based index of the tab to target. index < 1 wraps. If omitted, this tab.
|
||
*/
|
||
//#background
|
||
export async function tabduplicate(index?: number) {
|
||
return browser.tabs.duplicate(await idFromIndex(index))
|
||
}
|
||
|
||
/** Detach a tab, opening it in a new window.
|
||
|
||
@param index
|
||
The 1-based index of the tab to target. index < 1 wraps. If omitted, this tab.
|
||
*/
|
||
//#background
|
||
export async function tabdetach(index?: number) {
|
||
return browser.windows.create({ tabId: await idFromIndex(index) })
|
||
}
|
||
|
||
/** Toggle fullscreen state
|
||
|
||
*/
|
||
//#background
|
||
export async function fullscreen() {
|
||
// Could easily extend this to fullscreen / minimise any window but seems like that would be a tiny use-case.
|
||
const currwin = await browser.windows.getCurrent()
|
||
const wid = currwin.id
|
||
// This might have odd behaviour on non-tiling window managers, but no-one uses those, right?
|
||
const state = currwin.state === "fullscreen" ? "normal" : "fullscreen"
|
||
return browser.windows.update(wid, { state })
|
||
}
|
||
|
||
/** Close a tab.
|
||
|
||
Known bug: autocompletion will make it impossible to close more than one tab at once if the list of numbers looks enough like an open tab's title or URL.
|
||
|
||
@param indexes
|
||
The 1-based indexes of the tabs to target. indexes < 1 wrap. If omitted, this tab.
|
||
*/
|
||
//#background
|
||
export async function tabclose(...indexes: string[]) {
|
||
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)
|
||
}
|
||
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
|
||
*
|
||
*/
|
||
//#background
|
||
export async function tabcloseallto(side: string) {
|
||
if (!["left", "right"].includes(side)) {
|
||
throw new Error("side argument must be left or right")
|
||
}
|
||
const tabs = await browser.tabs.query({
|
||
pinned: false,
|
||
currentWindow: true,
|
||
})
|
||
|
||
const atab = await activeTab()
|
||
const comp = side == "right" ? tab => tab.index > atab.index : tab => tab.index < atab.index
|
||
const ids = tabs.filter(comp).map(tab => tab.id)
|
||
return browser.tabs.remove(ids)
|
||
}
|
||
|
||
/** Restore the most recently closed item.
|
||
The default behaviour is to restore the most recently closed tab in the
|
||
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. 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", "tab_strict" and "window".
|
||
@return
|
||
The tab or window id of the restored item. Returns -1 if no items are found.
|
||
*/
|
||
//#background
|
||
export async function undo(item = "recent"): Promise<number> {
|
||
const current_win_id: number = (await browser.windows.getCurrent()).id
|
||
const sessions = await browser.sessions.getRecentlyClosed()
|
||
|
||
// 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 (session) {
|
||
const restore = await browser.sessions.restore((session.tab || session.window).sessionId)
|
||
return (restore.tab || restore.window).id
|
||
}
|
||
return -1
|
||
}
|
||
|
||
/** Move the current tab to be just in front of the index specified.
|
||
|
||
Known bug: This supports relative movement with `tabmove +pos` and `tabmove -pos`, but autocomplete doesn't know that yet and will override positive and negative indexes.
|
||
|
||
Put a space in front of tabmove if you want to disable completion and have the relative indexes at the command line.
|
||
|
||
Binds are unaffected.
|
||
|
||
@param index
|
||
New index for the current tab.
|
||
|
||
1,start,^ are aliases for the first index. 0,end,$ are aliases for the last index.
|
||
*/
|
||
//#background
|
||
export async function tabmove(index = "$") {
|
||
const aTab = await activeTab()
|
||
const windowTabs = await browser.tabs.query({ currentWindow: true })
|
||
const windowPinnedTabs = await browser.tabs.query({ currentWindow: true, pinned: true })
|
||
const maxPinnedIndex = windowPinnedTabs.length - 1
|
||
|
||
let minindex: number
|
||
let maxindex: number
|
||
|
||
if (aTab.pinned) {
|
||
minindex = 0
|
||
maxindex = maxPinnedIndex
|
||
} else {
|
||
minindex = maxPinnedIndex + 1
|
||
maxindex = windowTabs.length - 1
|
||
}
|
||
|
||
let newindex: number
|
||
let relative = false
|
||
|
||
if (index.startsWith("+") || index.startsWith("-")) {
|
||
relative = true
|
||
newindex = Number(index) + aTab.index
|
||
} else if (["end", "$", "0"].includes(index)) {
|
||
newindex = maxindex
|
||
} else if (["start", "^"].includes(index)) {
|
||
newindex = 0
|
||
} else {
|
||
newindex = Number(index) + minindex - 1
|
||
}
|
||
|
||
if (newindex > maxindex) {
|
||
if (relative) {
|
||
while (newindex > maxindex) {
|
||
newindex -= maxindex - minindex + 1
|
||
}
|
||
} else newindex = maxindex
|
||
}
|
||
|
||
if (newindex < minindex) {
|
||
if (relative) {
|
||
while (newindex < minindex) {
|
||
newindex += maxindex - minindex + 1
|
||
}
|
||
} else newindex = minindex
|
||
}
|
||
|
||
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() {
|
||
const aTab = await activeTab()
|
||
return browser.tabs.update(aTab.id, { pinned: !aTab.pinned })
|
||
}
|
||
|
||
/** Mute current tab or all tabs.
|
||
|
||
Passing "all" to the excmd will operate on the mute state of all tabs.
|
||
Passing "unmute" to the excmd will unmute.
|
||
Passing "toggle" to the excmd will toggle the state of `browser.tabs.tab.MutedInfo`
|
||
@param string[] muteArgs
|
||
*/
|
||
//#background
|
||
export async function mute(...muteArgs: string[]): Promise<void> {
|
||
let mute = true
|
||
let toggle = false
|
||
let all = false
|
||
|
||
const argParse = (args: string[]) => {
|
||
if (args === null) {
|
||
return
|
||
}
|
||
if (args[0] === "all") {
|
||
all = true
|
||
args.shift()
|
||
argParse(args)
|
||
}
|
||
if (args[0] === "unmute") {
|
||
mute = false
|
||
args.shift()
|
||
argParse(args)
|
||
}
|
||
if (args[0] === "toggle") {
|
||
toggle = true
|
||
args.shift()
|
||
argParse(args)
|
||
}
|
||
}
|
||
|
||
argParse(muteArgs)
|
||
|
||
const updateObj = { muted: false }
|
||
if (mute) {
|
||
updateObj.muted = true
|
||
}
|
||
let done: Promise<any>
|
||
if (all) {
|
||
const tabs = await browser.tabs.query({ currentWindow: true })
|
||
const promises = []
|
||
for (const tab of tabs) {
|
||
if (toggle) {
|
||
updateObj.muted = !tab.mutedInfo.muted
|
||
}
|
||
promises.push(browser.tabs.update(tab.id, updateObj))
|
||
}
|
||
done = Promise.all(promises)
|
||
} else {
|
||
const tab = await activeTab()
|
||
if (toggle) {
|
||
updateObj.muted = !tab.mutedInfo.muted
|
||
}
|
||
done = browser.tabs.update(tab.id, updateObj)
|
||
}
|
||
return done.then(() => undefined)
|
||
}
|
||
// }}}
|
||
|
||
// {{{ WINDOWS
|
||
/**
|
||
* Like [[tabopen]], but in a new window.
|
||
*
|
||
* `winopen -private [...]` will open the result in a private window (and won't store the command in your ex-history ;) ).
|
||
*
|
||
* `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 Parameters<typeof browser.windows.create>[0]
|
||
let firefoxArgs = "--new-window"
|
||
let done = false
|
||
let useContainer = false
|
||
while (!done) {
|
||
switch (args[0]) {
|
||
case "-private":
|
||
createData.incognito = true
|
||
args.shift()
|
||
firefoxArgs = "--private-window"
|
||
break
|
||
|
||
case "-popup":
|
||
createData.type = "popup"
|
||
args.shift()
|
||
break
|
||
|
||
case "-c":
|
||
if (args.length < 2) throw new Error(`You must provide a container name!`)
|
||
args.shift()
|
||
useContainer = true
|
||
break
|
||
|
||
default:
|
||
done = true
|
||
break
|
||
}
|
||
}
|
||
|
||
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(" ")))
|
||
}
|
||
|
||
/**
|
||
* Close a window.
|
||
*
|
||
* @param id - The window id. Defaults to the id of the current window.
|
||
*
|
||
* Example: `winclose`
|
||
*/
|
||
//#background
|
||
export async function winclose(...ids: string[]) {
|
||
if (ids.length === 0) {
|
||
ids.push(`${(await browser.windows.getCurrent()).id}`)
|
||
}
|
||
return Promise.all(ids.map(id => browser.windows.remove(parseInt(id, 10))))
|
||
}
|
||
|
||
/**
|
||
* Add/change a prefix to the current window title
|
||
*
|
||
* Example: `wintitle [Hovercraft research]`
|
||
*
|
||
* Protip: unicode emojis work :)
|
||
*/
|
||
//#background
|
||
export async function wintitle(...title: string[]) {
|
||
const id = (await browser.windows.getCurrent()).id
|
||
return browser.windows.update(id, { titlePreface: title.join(" ") + " " })
|
||
}
|
||
|
||
/** Close all windows */
|
||
// It's unclear if this will leave a session that can be restored.
|
||
// We might have to do it ourselves.
|
||
//#background
|
||
export async function qall() {
|
||
const windows = await browser.windows.getAll()
|
||
return Promise.all(windows.map(window => browser.windows.remove(window.id)))
|
||
}
|
||
|
||
// }}}
|
||
|
||
/**
|
||
* EXPERIMENTAL: like [[open]] but loads queries in the sidebar. Doesn't actually open the sidebar - see [[sidebartoggle]].
|
||
*
|
||
* Not all schemas are supported, such as `about:*` and Firefox's built-in search engines. Tridactyl's searchurls and jsurls work fine - `:set searchengine google` will be sufficient for most users.
|
||
*
|
||
* If you try to open the command line in the sidebar things will break.
|
||
*/
|
||
//#background
|
||
export async function sidebaropen(...urllike: string[]) {
|
||
const url = await queryAndURLwrangler(urllike)
|
||
if (typeof url === "string") return browser.sidebarAction.setPanel({panel: url})
|
||
throw new Error("Unsupported URL for sidebar. If it was a search term try `:set searchengine google` first")
|
||
}
|
||
|
||
/**
|
||
* Like [[jsb]] but preserves "user action" intent for use with certain web extension APIs. Can only be called with browser mode binds, e.g.
|
||
*
|
||
* `:bind --mode=browser <C-.> jsua browser.sidebarAction.open(); tri.excmds.sidebaropen("https://mail.google.com/mail/mu")`
|
||
*/
|
||
//#background
|
||
export async function jsua(){
|
||
throw new Error(":jsua can only be called through `bind --mode=browser` binds, see `:help jsua`")
|
||
}
|
||
|
||
/**
|
||
* Toggle the side bar. Can only be called through browser mode binds, e.g.
|
||
*
|
||
* `:bind --mode=browser <C-.> sidebartoggle`
|
||
*/
|
||
//#background
|
||
export async function sidebartoggle(){
|
||
throw new Error(":sidebartoggle can only be called through `bind --mode=browser` binds, see `:help sidebartoggle`")
|
||
}
|
||
|
||
// {{{ CONTAINERS
|
||
|
||
/** Closes all tabs open in the same container across all windows.
|
||
@param name The container name.
|
||
*/
|
||
//#background
|
||
export async function containerclose(name: string) {
|
||
const containerId = await Container.getId(name)
|
||
return browser.tabs.query({ cookieStoreId: containerId }).then(tabs => browser.tabs.remove(tabs.map(tab => tab.id)))
|
||
}
|
||
/** Creates a new container. Note that container names must be unique and that the checks are case-insensitive.
|
||
|
||
Further reading https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/contextualIdentities/ContextualIdentity
|
||
|
||
Example usage:
|
||
- `:containercreate tridactyl green dollar`
|
||
|
||
@param name The container name. Must be unique.
|
||
@param color The container color. Valid colors are: "blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple". If no color is chosen a random one will be selected from the list of valid colors.
|
||
@param icon The container icon. Valid icons are: "fingerprint", "briefcase", "dollar", "cart", "circle", "gift", "vacation", "food", "fruit", "pet", "tree", "chill". If no icon is chosen, it defaults to "fingerprint".
|
||
*/
|
||
//#background
|
||
export async function containercreate(name: string, color?: string, icon?: string) {
|
||
await Container.create(name, color, icon)
|
||
}
|
||
|
||
/** Delete a container. Closes all tabs associated with that container beforehand. Note: container names are case-insensitive.
|
||
@param name The container name.
|
||
*/
|
||
//#background
|
||
export async function containerdelete(name: string) {
|
||
if (name == undefined) return
|
||
await containerclose(name)
|
||
await Container.remove(name)
|
||
}
|
||
|
||
/** Update a container's information. Note that none of the parameters are optional and that container names are case-insensitive.
|
||
|
||
Example usage:
|
||
|
||
- Changing the container name: `:containerupdate banking blockchain green dollar`
|
||
|
||
- Changing the container icon: `:containerupdate banking banking green briefcase`
|
||
|
||
- Changing the container color: `:containerupdate banking banking purple dollar`
|
||
|
||
@param name The container name.
|
||
@param uname The new container name. Must be unique.
|
||
@param ucolor The new container color. Valid colors are: "blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple". If no color is chosen a random one will be selected from the list of valid colors.
|
||
@param uicon The new container icon. Valid icons are: "fingerprint", "briefcase", "dollar", "cart", "circle", "gift", "vacation", "food", "fruit", "pet", "tree", "chill".
|
||
*/
|
||
//#background
|
||
export async function containerupdate(name: string, uname: string, ucolor: string, uicon: string) {
|
||
logger.debug("containerupdate parameters: " + name + ", " + uname + ", " + ucolor + ", " + uicon)
|
||
const containerId = await Container.fuzzyMatch(name)
|
||
const containerObj = Container.fromString(uname, ucolor, uicon)
|
||
Container.update(containerId, containerObj)
|
||
}
|
||
|
||
/** Shows a list of the current containers in Firefox's native JSON viewer in the current tab.
|
||
|
||
NB: Tridactyl cannot run on this page!
|
||
|
||
*/
|
||
//#content
|
||
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.
|
||
jsonview(JSON.stringify(containers))
|
||
}
|
||
|
||
/** Opens the current tab in another container.
|
||
|
||
This is probably not a good idea if you care about tracking protection!
|
||
Transfering URLs from one container to another allows websites to track
|
||
you across those containers.
|
||
|
||
Read more here:
|
||
https://github.com/mozilla/multi-account-containers/wiki/Moving-between-containers
|
||
|
||
@param containerName The container name, fuzzy matched like `-c` on [[tabopen]]. Leave empty to uncontain.
|
||
*/
|
||
//#background
|
||
export async function recontain(containerName: string) {
|
||
const thisTab = await activeTab()
|
||
|
||
let container
|
||
await Container.fuzzyMatch(containerName)
|
||
.then(match => {
|
||
container = match
|
||
})
|
||
.catch(() => {
|
||
container = Container.DefaultContainer.cookieStoreId
|
||
})
|
||
|
||
await openInNewTab(thisTab.url, {
|
||
active: true,
|
||
related: true,
|
||
cookieStoreId: container,
|
||
})
|
||
return browser.tabs.remove(thisTab.id)
|
||
}
|
||
|
||
// }}}
|
||
|
||
// {{{ TAB GROUPS
|
||
/** @hidden */
|
||
//#background_helper
|
||
// {
|
||
browser.tabs.onCreated.addListener(tgroupHandleTabCreated)
|
||
browser.tabs.onRemoved.addListener(tgroupHandleTabRemoved)
|
||
browser.tabs.onDetached.addListener(tgroupHandleTabDetached)
|
||
browser.tabs.onAttached.addListener(tgroupHandleTabAttached)
|
||
browser.tabs.onActivated.addListener(tgroupHandleTabActivated)
|
||
browser.tabs.onUpdated.addListener(tgroupHandleTabUpdated)
|
||
// }
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export function setContentStateGroup(name: string) {
|
||
contentState.group = name
|
||
}
|
||
|
||
/**
|
||
* Create a new tab group in the current window. NB: use [[tgroupswitch]] instead
|
||
* in most cases, since it will create non-existent tab groups before switching
|
||
* to them.
|
||
*
|
||
* Tab groups are a way of organizing different groups of related tabs within a
|
||
* single window. Groups allow you to have different named contexts and show
|
||
* only the tabs for a single group at a time.
|
||
*
|
||
* @param name The name of the tab group to create.
|
||
*
|
||
* If no tab groups exist, set the tab group name for all existing tabs in the
|
||
* window. Otherwise open a new tab and hide all tabs in the old tab group.
|
||
*
|
||
* Tab groups exist only for a single window.
|
||
*
|
||
*/
|
||
//#background
|
||
export async function tgroupcreate(name: string) {
|
||
const promises = []
|
||
const groups = await tgroups()
|
||
|
||
if (groups.has(name) || name === "#") {
|
||
throw new Error(`Tab group "${name}" already exists`)
|
||
}
|
||
|
||
if (groups.size > 0) {
|
||
await setWindowTgroup(name)
|
||
const initialUrl = await config.get("tabgroupnewtaburls")[name]
|
||
await tabopen(initialUrl)
|
||
promises.push(tgroupTabs(name, true).then(tabs => browserBg.tabs.hide(tabs.map(tab => tab.id))))
|
||
} else {
|
||
promises.push(
|
||
browser.tabs.query({ currentWindow: true, pinned: false }).then(tabs => {
|
||
setTabTgroup(
|
||
name,
|
||
tabs.map(({ id }) => id),
|
||
)
|
||
// trigger status line update
|
||
setContentStateGroup(name)
|
||
}),
|
||
)
|
||
promises.push(setWindowTgroup(name))
|
||
}
|
||
|
||
groups.add(name)
|
||
promises.push(setTgroups(groups))
|
||
return Promise.all(promises).then(() => name)
|
||
}
|
||
|
||
/**
|
||
* Switch to a different tab group, hiding all other tabs.
|
||
*
|
||
* "%" denotes the current tab group and "#" denotes the tab group that was
|
||
* last active. "A" indates a tab group that contains an audible tab. Use
|
||
* `:set completions.Tab.statusstylepretty true` to display a unicode character
|
||
* instead.
|
||
*
|
||
* @param name The name of the tab group to switch to.
|
||
*
|
||
* If the tab group does not exist, act like [[tgroupcreate]].
|
||
*
|
||
*/
|
||
//#background
|
||
export async function tgroupswitch(name: string) {
|
||
if (name === "#") {
|
||
return tgrouplast().then(() => name)
|
||
}
|
||
if (name == (await windowTgroup())) {
|
||
return
|
||
}
|
||
|
||
const groups = await tgroups()
|
||
if (groups.size > 0) {
|
||
if (groups.has(name)) {
|
||
return tgroupActivate(name).then(() => name)
|
||
} else {
|
||
return tgroupcreate(name).then(() => name)
|
||
}
|
||
} else {
|
||
return tgroupcreate(name).then(() => name)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Switch to the previously active tab group.
|
||
*/
|
||
//#background
|
||
export async function tgrouplast() {
|
||
if ((await tgroups()).size < 2) {
|
||
throw new Error("No last tab group")
|
||
}
|
||
|
||
return tgroupActivateLast()
|
||
}
|
||
|
||
/**
|
||
* Rename the current tab group.
|
||
*
|
||
* @param name The new name of the tab group.
|
||
*
|
||
*/
|
||
//#background
|
||
export async function tgrouprename(name: string) {
|
||
if ((await tgroups()).size == 0) {
|
||
throw new Error("No tab groups exist")
|
||
}
|
||
|
||
return tgroupClearOldInfo(await windowTgroup(), name).then(() => {
|
||
// trigger status line update
|
||
setContentStateGroup(name)
|
||
return name
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Close all tabs in a tab group and delete the group.
|
||
*
|
||
* @param name The name of the tab group to close. If not specified, close the
|
||
* current tab group and switch to the previously active tab group.
|
||
*
|
||
* Do nothing if there is only one tab group - to discard all tab group
|
||
* information, use [[tgroupabort]].
|
||
*
|
||
*/
|
||
//#background
|
||
export async function tgroupclose(name?: string) {
|
||
const groups = await tgroups()
|
||
if (groups.size == 0) {
|
||
throw new Error("No tab groups exist")
|
||
} else if (groups.size == 1) {
|
||
throw new Error("This is the only tab group")
|
||
} else if (name !== undefined && name !== "#" && !groups.has(name)) {
|
||
throw new Error(`No tab group named "${name}"`)
|
||
} else if (groups.size > 1) {
|
||
const currentGroup = await windowTgroup()
|
||
let closeGroup = currentGroup
|
||
if (name === "#") {
|
||
closeGroup = await windowLastTgroup()
|
||
if (name === undefined) {
|
||
throw new Error("No alternate tab group")
|
||
}
|
||
} else if (name !== undefined) {
|
||
closeGroup = name
|
||
}
|
||
let newTabGroup = currentGroup
|
||
if (closeGroup === currentGroup) {
|
||
newTabGroup = await tgroupActivateLast()
|
||
}
|
||
await tgroupTabs(closeGroup).then(tabs => {
|
||
browser.tabs.remove(tabs.map(tab => tab.id))
|
||
})
|
||
return tgroupClearOldInfo(closeGroup).then(() => newTabGroup)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Move the current tab to another tab group, creating it if it does not exist.
|
||
*
|
||
* @param name The name of the tab group to move the tab to.
|
||
*
|
||
* If this is the last tab in the tab group, also switch to tab group, keeping
|
||
* the current tab active.
|
||
*
|
||
*/
|
||
//#background
|
||
export async function tgroupmove(name: string) {
|
||
const groups = await tgroups()
|
||
const currentGroup = await windowTgroup()
|
||
|
||
if (groups.size == 0) {
|
||
throw new Error("No tab groups exist")
|
||
}
|
||
if (name == currentGroup) {
|
||
throw new Error(`Tab is already on group "${name}"`)
|
||
}
|
||
if (name === "#") {
|
||
name = await windowLastTgroup()
|
||
if (name === undefined) {
|
||
throw new Error("No alternate tab group")
|
||
}
|
||
}
|
||
if (!groups.has(name)) {
|
||
// Create new tab group if there isn't one with this name
|
||
groups.add(name)
|
||
await setTgroups(groups)
|
||
}
|
||
|
||
const tabCount = await tgroupTabs(currentGroup).then(tabs => tabs.length)
|
||
|
||
await setTabTgroup(name)
|
||
setContentStateGroup(name)
|
||
const currentTabId = await activeTabId()
|
||
|
||
// switch to other group if this is the last tab in the current group
|
||
if (tabCount == 1) {
|
||
return Promise.all([
|
||
tgroupClearOldInfo(currentGroup, name),
|
||
tgroupTabs(name).then(tabs => {
|
||
browserBg.tabs.show(tabs.map(tab => tab.id))
|
||
}),
|
||
]).then(() => name)
|
||
} else {
|
||
const lastTabId = await tgroupLastTabId(currentGroup)
|
||
await tabSetActive(lastTabId)
|
||
return browser.tabs.hide(currentTabId).then(() => currentGroup)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Delete all tab group information for the current window and show all tabs.
|
||
*
|
||
*/
|
||
//#background
|
||
export async function tgroupabort() {
|
||
if ((await tgroups()).size == 0) {
|
||
throw new Error("No tab groups exist")
|
||
}
|
||
|
||
return clearAllTgroupInfo().then(() => undefined)
|
||
}
|
||
|
||
// }}}
|
||
|
||
// {{{ MISC
|
||
|
||
//#background
|
||
export function version() {
|
||
return fillcmdline_notrail(TRI_VERSION)
|
||
}
|
||
|
||
/**
|
||
* Switch mode.
|
||
*
|
||
* For now you probably shouldn't manually switch to other modes than `normal` and `ignore`. Make sure you're aware of the key bindings (ignoremaps) that will allow you to go come back to normal mode from ignore mode before you run `:mode ignore` otherwise you're going to have a hard time re-enabling Tridactyl.
|
||
*
|
||
* Example:
|
||
* - `mode ignore` to ignore almost all keys.
|
||
*
|
||
* If you're looking for a way to temporarily disable Tridactyl, `mode ignore` might be what you're looking for.
|
||
*
|
||
* Note that when in ignore mode, Tridactyl will not switch to insert mode when focusing text areas/inputs. This is by design.
|
||
*
|
||
* **New feature:** you can add modes as simply as adding binds with `bind --mode=[newmodename]` and then enter the mode with `mode [newmodename]`.
|
||
*/
|
||
//#content
|
||
export function mode(mode: ModeName) {
|
||
// TODO: event emition on mode change.
|
||
if (mode === "hint") {
|
||
hint()
|
||
} else {
|
||
contentState.mode = mode
|
||
}
|
||
}
|
||
|
||
/** @hidden */
|
||
//#background_helper
|
||
async function getnexttabs(tabid: number, n?: number) {
|
||
const curIndex: number = (await browser.tabs.get(tabid)).index
|
||
const tabs: browser.tabs.Tab[] = await browser.tabs.query({
|
||
currentWindow: true,
|
||
})
|
||
const indexFilter = ((tab: browser.tabs.Tab) => curIndex <= tab.index && (n ? tab.index < curIndex + Number(n) : true)).bind(n)
|
||
return tabs.filter(indexFilter).map((tab: browser.tabs.Tab) => tab.id)
|
||
}
|
||
|
||
// Moderately slow; should load in results as they arrive, perhaps
|
||
// Todo: allow jumping to buffers once they are found
|
||
// Consider adding to buffers with incremental search
|
||
// maybe only if no other results in URL etc?
|
||
// Find out how to return context of each result
|
||
// //#background
|
||
/* export async function findintabs(query: string) { */
|
||
/* const tabs = await browser.tabs.query({currentWindow: true}) */
|
||
/* console.log(query) */
|
||
/* const findintab = async tab => */
|
||
/* await browser.find.find(query, {tabId: tab.id}) */
|
||
/* let results = [] */
|
||
/* for (let tab of tabs) { */
|
||
/* let result = await findintab(tab) */
|
||
/* if (result.count > 0) { */
|
||
/* results.push({tab, result}) */
|
||
/* } */
|
||
/* } */
|
||
/* results.sort(r => r.result.count) */
|
||
/* console.log(results) */
|
||
/* return results */
|
||
/* } */
|
||
|
||
// }}}
|
||
|
||
// {{{ CMDLINE
|
||
|
||
/** Repeats a `cmd` `n` times.
|
||
If `cmd` doesn't exist, re-executes the last exstr that was executed in the tab.
|
||
Executes the command once if `n` isn't defined either.
|
||
|
||
This re-executes the last *exstr*, not the last *excmd*. Some excmds operate internally by constructing and evaluating exstrs, others by directly invoking excmds without going through the exstr parser. For example, aucmds and keybindings evaluate exstrs and are repeatable, while commands like `:bmarks` directly invoke `:tabopen` and you'll repeat the `:bmarks` rather than the internal `:tabopen`.
|
||
|
||
It's difficult to execute this in the background script (`:jsb`, `:run_excmd`, `:autocmd TriStart`, `:source`), but if you you do, it will re-execute the last exstr that was executed in the background script. What this may have been is unpredictable and not precisely encouraged.
|
||
|
||
*/
|
||
//#background
|
||
export async function repeat(n = 1, ...exstr: string[]) {
|
||
let cmd = state.last_ex_str
|
||
if (exstr.length > 0) cmd = exstr.join(" ")
|
||
logger.debug("repeating " + cmd + " " + n + " times")
|
||
for (let i = 0; i < n; i++) {
|
||
await controller.acceptExCmd(cmd)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Split `cmds` on pipes (|) and treat each as its own command. Return values are passed as the last argument of the next ex command, e.g,
|
||
*
|
||
* `composite echo yes | fillcmdline` becomes `fillcmdline yes`. A more complicated example is the ex alias, `command current_url composite get_current_url | fillcmdline_notrail `, which is used in, e.g. `bind T current_url tabopen`.
|
||
*
|
||
* Workaround: this should clearly be in the parser, but we haven't come up with a good way to deal with |s in URLs, search terms, etc. yet.
|
||
*
|
||
* `cmds` are also split with semicolons (;) and don't pass things along to each other.
|
||
*
|
||
* If you wish to have a command that has semi-colons in it (e.g. some JavaScript or `hint -;`), first bind a [[command]] to it. For example, `command hint_focus -;`, and then `composite hint_focus; !s xdotool key Menu`.
|
||
*
|
||
* The behaviour of combining ; and | in the same composite command is left as an exercise for the reader.
|
||
*/
|
||
//#both
|
||
export async function composite(...cmds: string[]) {
|
||
try {
|
||
return (
|
||
cmds
|
||
.join(" ")
|
||
// Semicolons delimit pipelines
|
||
.split(";")
|
||
// For each pipeline, wait for previous pipeline to finish, then
|
||
// execute each cmd in pipeline in order, passing the result of the
|
||
// previous cmd as the last argument to the next command.
|
||
.reduce(async (prev_pipeline, cmd) => {
|
||
await prev_pipeline
|
||
const cmds = cmd.split("|")
|
||
|
||
// Compute the first piped value.
|
||
//
|
||
// We could invoke controller.acceptExCmd, but
|
||
// that would cause our pipeline section to be
|
||
// stored as the last executed command for the
|
||
// purposes of :repeat, which would be
|
||
// nonsense. So we copy-paste the important
|
||
// parts of the body of that function instead.
|
||
const [fn, args] = excmd_parser.parser(cmds[0], ALL_EXCMDS)
|
||
const first_value = fn.call({}, ...args)
|
||
|
||
// Exec the rest of the pipe in sequence.
|
||
return cmds.slice(1).reduce(async (pipedValue, cmd) => {
|
||
const [fn, args] = excmd_parser.parser(cmd, ALL_EXCMDS)
|
||
return fn.call({}, ...args, await pipedValue)
|
||
}, first_value)
|
||
}, null as any)
|
||
)
|
||
} catch (e) {
|
||
logger.error(e)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Escape command for safe use in shell with composite. E.g: `composite js MALICIOUS_WEBSITE_FUNCTION() | shellescape | exclaim ls`
|
||
*/
|
||
export async function shellescape(...quoteme: string[]) {
|
||
const str = quoteme.join(" ")
|
||
const os = (await browserBg.runtime.getPlatformInfo()).os
|
||
if (os === "win") {
|
||
return escape.windows_cmd(str)
|
||
} else {
|
||
return escape.sh(str)
|
||
}
|
||
}
|
||
|
||
//#background_helper
|
||
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.
|
||
*
|
||
* Only useful if called from a background context, e.g. at the end of an RC file to ensure that when you start the browser you don't get trapped on an about: page, or via `bind --mode=browser escapehatch` (bound to `<C-,>` by default).
|
||
*
|
||
* 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() {
|
||
useractions.escapehatch()
|
||
}
|
||
|
||
/** Sleep time_ms milliseconds.
|
||
* This is probably only useful for composite commands that need to wait until the previous asynchronous command has finished running.
|
||
*/
|
||
//#both
|
||
export function sleep(time_ms: number) {
|
||
return new Promise(resolve => setTimeout(resolve, time_ms))
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export function showcmdline(focus = true) {
|
||
logger.debug("excmds showcmdline()")
|
||
const hidehover = true
|
||
CommandLineContent.show(hidehover)
|
||
let done = Promise.resolve()
|
||
if (focus) {
|
||
done = Messaging.messageOwnTab("commandline_frame", "focus")
|
||
}
|
||
return done
|
||
}
|
||
|
||
/** @hidden */
|
||
//#content
|
||
export function hidecmdline() {
|
||
CommandLineContent.hide_and_blur()
|
||
}
|
||
|
||
/** Set the current value of the commandline to string *with* a trailing space */
|
||
//#content
|
||
export function fillcmdline(...strarr: string[]) {
|
||
const str = strarr.join(" ")
|
||
showcmdline(false)
|
||
logger.debug("excmds fillcmdline sending fillcmdline to commandline_frame")
|
||
return Messaging.messageOwnTab("commandline_frame", "fillcmdline", [str, true/*trailspace*/, true/*focus*/])
|
||
}
|
||
|
||
/** Set the current value of the commandline to string *without* a trailing space */
|
||
//#content
|
||
export function fillcmdline_notrail(...strarr: string[]) {
|
||
const str = strarr.join(" ")
|
||
showcmdline(false)
|
||
return Messaging.messageOwnTab("commandline_frame", "fillcmdline", [str, false/*trailspace*/, true/*focus*/])
|
||
}
|
||
|
||
/** Show and fill the command line without focusing it */
|
||
//#content
|
||
export function fillcmdline_nofocus(...strarr: string[]) {
|
||
showcmdline(false)
|
||
return Messaging.messageOwnTab("commandline_frame", "fillcmdline", [strarr.join(" "), false, false])
|
||
}
|
||
|
||
/** Shows str in the command line for ms milliseconds. Recommended duration: 3000ms. */
|
||
//#content
|
||
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<void>(resolve =>
|
||
setTimeout(async () => {
|
||
if ((await Messaging.messageOwnTab("commandline_frame", "getContent", [])) === str) {
|
||
CommandLineContent.hide_and_blur()
|
||
resolve(Messaging.messageOwnTab("commandline_frame", "clear", [true]))
|
||
}
|
||
resolve()
|
||
}, ms),
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Copy `content` to clipboard without feedback. Use `clipboard yank` for interactive use.
|
||
*
|
||
* e.g. `yank bob` puts "bob" in the clipboard; `composite js document.title | yank` puts the document title in the clipboard.
|
||
*/
|
||
//#background
|
||
export function yank(...content: string[]) {
|
||
return setclip(content.join(" "))
|
||
}
|
||
|
||
/**
|
||
* Copies a string to the clipboard/selection buffer depending on the user's preferences.
|
||
*
|
||
* @hidden
|
||
*/
|
||
//#background_helper
|
||
async function setclip(data: string) {
|
||
// Function to avoid retyping everything everywhere
|
||
const setclip_selection = data => Native.clipboard("set", data)
|
||
|
||
let promises: Promise<any>[]
|
||
switch (await config.getAsync("yankto")) {
|
||
case "selection":
|
||
promises = [setclip_selection(data)]
|
||
break
|
||
case "clipboard":
|
||
promises = [setclip_webapi(data)]
|
||
break
|
||
case "both":
|
||
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(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]].
|
||
|
||
If `excmd === "yank"`, copy the current URL, or if given, the value of toYank, into the system clipboard.
|
||
|
||
If `excmd === "yankcanon"`, copy the canonical URL of the current page if it exists, otherwise copy the current URL.
|
||
|
||
If `excmd === "yankshort"`, copy the shortlink version of the current URL, and fall back to the canonical then actual URL. Known to work on https://yankshort.neocities.org/.
|
||
|
||
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. `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).
|
||
|
||
When doing a read operation (i.e. open or tabopen), if "putfrom" is set to "selection", the X selection buffer will be read instead of the clipboard. Set "putfrom" to "clipboard" to use the clipboard.
|
||
|
||
When doing a write operation, if "yankto" is set to "selection", only the X selection buffer will be written to. If "yankto" is set to "both", both the X selection and the clipboard will be written to. If "yankto" is set to "clipboard", only the clipboard will be written to.
|
||
|
||
*/
|
||
//#background
|
||
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 = []
|
||
let done = Promise.resolve(undefined as any)
|
||
switch (excmd) {
|
||
case "yankshort":
|
||
urls = await geturlsforlinks("rel", "shortlink")
|
||
if (urls.length === 0) {
|
||
urls = await geturlsforlinks("rev", "canonical")
|
||
}
|
||
if (urls.length > 0) {
|
||
await yank(urls[0])
|
||
done = fillcmdline_tmp(3000, "# " + urls[0] + " copied to clipboard.")
|
||
break
|
||
}
|
||
// Trying yankcanon if yankshort failed...
|
||
case "yankcanon":
|
||
urls = await geturlsforlinks("rel", "canonical")
|
||
if (urls.length > 0) {
|
||
await yank(urls[0])
|
||
done = fillcmdline_tmp(3000, "# " + urls[0] + " copied to clipboard.")
|
||
break
|
||
}
|
||
// Trying yank if yankcanon failed...
|
||
case "yank":
|
||
content = content === "" ? (await activeTab()).url : content
|
||
await yank(content)
|
||
done = fillcmdline_tmp(3000, "# " + content + " copied to clipboard.")
|
||
break
|
||
case "yanktitle":
|
||
content = (await activeTab()).title
|
||
await yank(content)
|
||
done = fillcmdline_tmp(3000, "# " + content + " copied to clipboard.")
|
||
break
|
||
case "yankmd":
|
||
content = "[" + (await activeTab()).title + "](" + (await activeTab()).url + ")"
|
||
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) {
|
||
done = open(url.trim())
|
||
}
|
||
break
|
||
case "tabopen":
|
||
url = await getclip()
|
||
if (url) {
|
||
done = tabopen(url.trim())
|
||
}
|
||
break
|
||
case "xselpaste":
|
||
content = await getclip("selection")
|
||
if (content.length > 0) {
|
||
EditorCmds.insert_text(content)
|
||
}
|
||
break
|
||
default:
|
||
// todo: maybe we should have some common error and error handler
|
||
throw new Error(`[clipboard] unknown excmd: ${excmd}`)
|
||
}
|
||
return done.then(() => undefined)
|
||
}
|
||
|
||
/** Copy an image to the clipboard.
|
||
|
||
@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 yankimage(url: string): Promise<void> {
|
||
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 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.
|
||
|
||
"%" denotes the current tab and "#" denotes the tab that was last accessed in this window. "P", "A", "M" and "D" indicate tab status (i.e. a pinned, audible, muted or discarded tab). Use `:set completions.Tab.statusstylepretty true` to display unicode characters instead. "P","A","M","D" can be used to filter by tab status in either setting.
|
||
|
||
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 taball. Title matches can contain '*' as a wildcard.
|
||
*/
|
||
//#background
|
||
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 })
|
||
return browser.tabs.update(tabid, { active: true })
|
||
}
|
||
|
||
// }}}
|
||
|
||
// }}}
|
||
|
||
// {{{ SETTINGS
|
||
|
||
/**
|
||
* Similar to vim's `:command`. Maps one ex-mode command to another.
|
||
* If command already exists, this will override it, and any new commands
|
||
* added in a future release will be SILENTLY overridden. Aliases are
|
||
* expanded recursively.
|
||
*
|
||
* Examples:
|
||
* - `command t tabopen`
|
||
* - `command tn tabnext_gt`
|
||
* - `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`. 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
|
||
* command to an excommand, see [[bind]].
|
||
*
|
||
* See also:
|
||
* - [[comclear]]
|
||
*/
|
||
//#background
|
||
export function command(name: string, ...definition: string[]) {
|
||
// Test if alias creates an alias loop.
|
||
try {
|
||
const def = definition.join(" ")
|
||
aliases.expandExstr(name)
|
||
return config.set("exaliases", name, def)
|
||
} catch (e) {
|
||
config.unset("exaliases", name)
|
||
throw new Error(`Alias not set. ${e}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Similar to vim's `comclear` command. Clears an excmd alias defined by
|
||
* `command`.
|
||
*
|
||
* For example: `comclear helloworld` will reverse any changes caused
|
||
* by `command helloworld xxx`
|
||
*
|
||
* See also:
|
||
* - [[command]]
|
||
*/
|
||
//#background
|
||
export function comclear(name: string) {
|
||
config.unset("exaliases", name)
|
||
}
|
||
|
||
/** Bind a sequence of keys to an excmd or view bound sequence.
|
||
|
||
This is an easier-to-implement bodge while we work on vim-style maps.
|
||
|
||
Examples:
|
||
|
||
- `bind G fillcmdline tabopen google`
|
||
- `bind D composite tabclose | tab #` -> close current tab and switch to most recent previous tab
|
||
- `bind j scrollline 20`
|
||
- `bind F hint -b`
|
||
|
||
You can view binds by omitting the command line:
|
||
|
||
- `bind j`
|
||
- `bind k`
|
||
|
||
You can bind to modifiers and special keys by enclosing them with angle brackets, for example `bind <C-\>z fullscreen`, `unbind <F1>` (a favourite of people who use TreeStyleTabs :) ), or `bind <Backspace> forward`.
|
||
|
||
Modifiers are truncated to a single character, so Ctrl -> C, Alt -> A, and Shift -> S. Shift is a bit special as it is only required if Shift does not change the key inputted, e.g. `<S-ArrowDown>` is OK, but `<S-a>` should just be `A`.
|
||
|
||
You can view all special key names here: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||
|
||
Use [[composite]] if you want to execute multiple excmds. Use
|
||
[[fillcmdline]] to put a string in the cmdline and focus the cmdline
|
||
(otherwise the string is executed immediately).
|
||
|
||
You can bind to other modes with `bind --mode={insert|ignore|normal|input|ex|hint} ...`, e.g, `bind --mode=insert emacs qall` (NB: unlike vim, all preceeding characters will not be input), or `bind --mode=hint <C-[> hint.reset`.
|
||
|
||
`bind --mode=browser [key sequence] [ex command]` binds to a special mode which can be accessed all the time in all browser tabs - even tabs in which Tridactyl cannot run. It comes with a few caveats:
|
||
|
||
- you may only have a few browser-mode binds at once. At the time of writing, this is 20, with 3 initially taken by Tridactyl. If you desperately need more, file an [[issue]].
|
||
- the key sequence must consist of a single, simple key with at least one and no more than two modifiers. An error will be thrown if you try to bind to an invalid key sequence.
|
||
- the `ex command` you bind to may not work fully unless you are on a tab which Tridactyl has access to. Generally, browser-wide actions like making or closing tabs will work but tab-specific actions like scrolling down or entering hint mode will not.
|
||
|
||
A list of editor functions can be found
|
||
[here](/static/docs/modules/_src_lib_editor_.html).
|
||
|
||
See also:
|
||
|
||
- [[unbind]]
|
||
- [[reset]]
|
||
*/
|
||
//#background
|
||
export async function bind(...args: string[]) {
|
||
const args_obj = parse_bind_args(...args)
|
||
let p = Promise.resolve()
|
||
if (args_obj.excmd !== "") {
|
||
for (let i = 0; i < args_obj.key.length; i++) {
|
||
// Check if any initial subsequence of the key exists and will shadow the new binding
|
||
const key_sub = args_obj.key.slice(0, i)
|
||
if (config.getDynamic(args_obj.configName, key_sub)) {
|
||
fillcmdline_notrail("# Warning: bind `" + key_sub + "` exists and will shadow `" + args_obj.key + "`. Try running `:unbind --mode=" + args_obj.mode + " " + key_sub + "`")
|
||
break
|
||
}
|
||
}
|
||
if (args_obj.mode == "browser") {
|
||
const commands = await browser.commands.getAll()
|
||
|
||
// Check for an existing command with this bind
|
||
let command = commands.filter(c => mozMapToMinimalKey(c.shortcut).toMapstr() == args_obj.key)[0]
|
||
|
||
// If there isn't one, find an unused command
|
||
command = command === undefined ? (command = commands.filter(c => c.shortcut === "")[0]) : command
|
||
if (command === undefined) throw new Error("You have reached the maximum number of browser binds. `:unbind` one you don't want from `:viewconfig browsermaps`.")
|
||
|
||
await browser.commands.update({ name: command.name, shortcut: minimalKeyToMozMap(mapstrToKeyseq(args_obj.key)[0]) })
|
||
await commandsHelper.updateListener()
|
||
}
|
||
p = config.set(args_obj.configName, args_obj.key, args_obj.excmd)
|
||
} else if (args_obj.key.length) {
|
||
// Display the existing bind
|
||
p = bindshow(...args)
|
||
}
|
||
return p
|
||
}
|
||
|
||
/*
|
||
* Show what ex-command a key sequence is currently bound to
|
||
*/
|
||
//#background
|
||
export function bindshow(...args: string[]) {
|
||
const args_obj = parse_bind_args(...args)
|
||
return fillcmdline_notrail("bind", (args_obj.mode ? "--mode=" + args_obj.mode + " " : "") + args_obj.key, config.getDynamic(args_obj.configName, args_obj.key))
|
||
}
|
||
|
||
/**
|
||
Generate a key sequence from keypresses. Once Enter is pressed, the command line is filled with a [[bind]]
|
||
command with the key sequence and provided arguments, which you can choose to modify and execute.
|
||
|
||
If you have `:set keyboardlayoutforce true`, it will bind commands to physical keys regardless of layout.
|
||
|
||
Accepts the same arguments as [[bind]] (except for the key sequence which will be generated):
|
||
|
||
- `bindwizard [command]`, then press the keys you want to bind, then hit Enter.
|
||
- `bindwizard --mode=[mode] [command]` also works.
|
||
|
||
You can execute it without arguments to see what is bound to the keys you type.
|
||
*/
|
||
export async function bindwizard(...args: string[]) {
|
||
// TODO: this should use parse_bind_args in case we ever support e.g. --url=
|
||
let mode = "normal"
|
||
if (args.length && args[0].startsWith("--mode=")) {
|
||
mode = args.shift().replace("--mode=", "")
|
||
}
|
||
return gobble("<CR>", `fillcmdline_notrail bind --mode=${mode}`, ...args)
|
||
}
|
||
|
||
/**
|
||
* Like [[bind]] but for a specific url pattern (also see [[seturl]]).
|
||
*
|
||
* @param pattern Mandatory. The regex pattern on which the binding should take effect.
|
||
* @param mode Optional. The mode the binding should be in (e.g. normal, insert, ignore, input). Defaults to normal.
|
||
* @param keys Mandatory. The keys that should be bound.
|
||
* @param excmd Optional. Without it, will display what `keys` are bound to in `mode`.
|
||
*
|
||
*/
|
||
//#background
|
||
export function bindurl(pattern: string, mode: string, keys: string, ...excmd: string[]) {
|
||
const args_obj = parse_bind_args(mode, keys, ...excmd)
|
||
if (args_obj.mode === "browser") throw new Error("Browser-wide binds are not supported per-URL")
|
||
let p = Promise.resolve()
|
||
if (args_obj.excmd !== "") {
|
||
p = config.setURL(pattern, args_obj.configName, args_obj.key, args_obj.excmd)
|
||
} else if (args_obj.key.length) {
|
||
// Display the existing bind
|
||
p = fillcmdline_notrail("#", args_obj.key, "=", config.getURL(pattern, [args_obj.configName, args_obj.key]))
|
||
}
|
||
return p
|
||
}
|
||
|
||
/**
|
||
* Deprecated: use `:set keyboardlayoutforce true` instead.
|
||
*
|
||
* 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
|
||
*
|
||
*/
|
||
//#background
|
||
export function keymap(source: string, target: string) {
|
||
if (config.get("keyboardlayoutforce") == "true") {
|
||
fillcmdline("You can't keymap with keyboardlayoutforce set. Set values in keyboardlayoutoverrides to change layout for tridactyl shortcuts.")
|
||
return
|
||
}
|
||
return set("keytranslatemap." + source, target)
|
||
}
|
||
|
||
/**
|
||
* @hidden
|
||
*/
|
||
//#background
|
||
export function searchsetkeyword() {
|
||
throw new Error(":searchsetkeyword has been deprecated. Use `set searchurls.KEYWORD URL` instead.")
|
||
}
|
||
|
||
/**
|
||
* Validates arguments for set/seturl
|
||
* @hidden
|
||
*/
|
||
function validateSetArgs(key: string, values: string[]) {
|
||
const target: any[] = key.split(".")
|
||
|
||
let value
|
||
const file = Metadata.everything.getFile("src/lib/config.ts")
|
||
const default_config = file.getClass("default_config")
|
||
const md = default_config.getMember(target[0])
|
||
if (md !== undefined) {
|
||
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.type as ObjectType).convertMember(target.slice(1), strval)
|
||
} else {
|
||
value = md.type.convert(strval)
|
||
}
|
||
} else {
|
||
// If we don't have metadata, fall back to the old way
|
||
logger.warning("Could not fetch setting metadata. Falling back to type of current value.")
|
||
const currentValue = config.get(...target)
|
||
if (Array.isArray(currentValue)) {
|
||
// Do nothing
|
||
} else if (currentValue === undefined || typeof currentValue === "string") {
|
||
value = values.join(" ")
|
||
} else {
|
||
throw new Error("Unsupported setting type!")
|
||
}
|
||
}
|
||
|
||
target.push(value)
|
||
return target
|
||
}
|
||
|
||
/**
|
||
* Usage: `seturl [pattern] key values`
|
||
*
|
||
* @param pattern The URL regex pattern the setting should be set for, e.g. `^https://en.wikipedia.org` or `/index.html`. Defaults to the current url if `values` is a single word.
|
||
* @param key The name of the setting you want to set, e.g. `followpagepatterns.next`
|
||
* @param values The value you wish for, e.g. `next`
|
||
*
|
||
* Example:
|
||
* - `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[]) {
|
||
if (values.length === 0 && key) {
|
||
values = [key]
|
||
key = pattern
|
||
pattern = window.location.href
|
||
}
|
||
|
||
if (!pattern || !key || !values.length) {
|
||
throw new Error("seturl syntax: [pattern] key value")
|
||
}
|
||
|
||
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).
|
||
|
||
Arrays should be set using JS syntax, e.g. `:set blacklistkeys ["/",","]`.
|
||
|
||
e.g.
|
||
set searchurls.google https://www.google.com/search?q=
|
||
set logging.messaging info
|
||
|
||
If no value is given, the value of the of the key will be displayed.
|
||
|
||
See also: [[unset]]
|
||
*/
|
||
//#background
|
||
export function set(key: string, ...values: string[]) {
|
||
if (!key) {
|
||
throw new Error("Key must be provided!")
|
||
} else if (!values[0]) {
|
||
return get(key)
|
||
}
|
||
|
||
if (key === "noiframeon") {
|
||
const noiframes = config.get("noiframeon")
|
||
// unset previous settings
|
||
if (noiframes) noiframes.forEach(url => seturl(url, "noiframe", "false"))
|
||
// store new settings
|
||
values.forEach(url => seturl(url, "noiframe", "true"))
|
||
// save as deprecated setting for compatibility
|
||
config.set("noiframeon", values)
|
||
throw new Error("Warning: `noiframeon $url1 $url2` has been deprecated in favor of `:seturl $url1 noiframe true`. The right seturl calls have been made for you but from now on please use `:seturl`.")
|
||
}
|
||
|
||
if (key === "csp" && values[0] === "clobber") {
|
||
const msg = "#Error: Mozilla asked us to remove our csp-clobbering code. See https://github.com/tridactyl/tridactyl/issues/1800"
|
||
fillcmdline_tmp(3000, msg)
|
||
throw msg
|
||
}
|
||
|
||
const target = validateSetArgs(key, values)
|
||
|
||
key === "proxy" && Proxy.exists(target.slice(-1))
|
||
|
||
return config.set(...target)
|
||
}
|
||
|
||
/**
|
||
* Replaces your local configuration with that stored in the Firefox Sync area.
|
||
*
|
||
* It does not merge your configurations: it overwrites.
|
||
*
|
||
* Also see [[firefoxsyncpush]].
|
||
*/
|
||
//#background
|
||
export function firefoxsyncpull() {
|
||
return config.pull()
|
||
}
|
||
|
||
/**
|
||
* Pushes your local configuration to the Firefox Sync area.
|
||
*
|
||
* It does not merge your configurations: it overwrites.
|
||
*
|
||
* Also see [[firefoxsyncpull]].
|
||
*/
|
||
//#background
|
||
export function firefoxsyncpush() {
|
||
return config.push()
|
||
}
|
||
|
||
/** @hidden */
|
||
//#background_helper
|
||
const AUCMDS = ["DocStart", "DocLoad", "DocEnd", "TriStart", "TabEnter", "TabLeft", "FullscreenChange", "FullscreenEnter", "FullscreenLeft", "UriChange", "HistoryState"].concat(webrequests.requestEvents)
|
||
/** @hidden */
|
||
//#background_helper
|
||
export function getAutocmdEvents() {
|
||
return AUCMDS
|
||
}
|
||
/** Set autocmds to run when certain events happen.
|
||
*
|
||
* @param event Currently, 'TriStart', 'DocStart', 'DocLoad', 'DocEnd', 'TabEnter', 'TabLeft', 'FullscreenChange', 'FullscreenEnter', 'FullscreenLeft', 'HistoryState', 'HistoryPushState', 'HistoryReplace', 'UriChange', 'AuthRequired', 'BeforeRedirect', 'BeforeRequest', 'BeforeSendHeaders', 'Completed', 'ErrorOccured', 'HeadersReceived', 'ResponseStarted', and 'SendHeaders' are supported
|
||
*
|
||
- DocStart: When a webpage loading. Exactly, when tridactyl is loading in a page.
|
||
- DocLoad: When the whole html parsed, not including image/css loaded. (Just like jquery $(fn) or the [DOMContentLoaded event](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event).)
|
||
- DocEnd: When a webpage unloaded/closed or backward/forward in history. Exactly, the [pagehide event](https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event).
|
||
- TabEnter: When a tab get focus.
|
||
- TabLeft: When a tab lost focus or closed.
|
||
*
|
||
* The 'HistoryState' event is triggered when a page uses the web history API to change the page location / URI. It should be used in preference to 'UriChange' below since it will use almost no resources. The 'UriChange' event may work on websites where 'HistoryState' does not.
|
||
*
|
||
* The 'HistoryPushState' is triggered only when a page call 'history.pushState' to change URI, and 'HistoryReplace' is for 'history.replace'. By the way, the HistoryPopState is not implemented.
|
||
*
|
||
* 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 (!getAutocmdEvents().includes(event)) throw new Error(event + " is not a supported event.")
|
||
return config.set("autocmds", event, url, excmd.join(" "))
|
||
}
|
||
|
||
/**
|
||
* Automatically open a domain and all its subdomains in a specified container.
|
||
*
|
||
* __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 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 (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`. If multiple rules match a certain URL, the one with the longest regex will be picked.
|
||
*
|
||
* 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.
|
||
*
|
||
* Pass an optional space-separated list of proxy names to assign a proxy (followed by failover proxies) to a URL and open in a specified container.
|
||
* For example: `autocontain [-{u,s}] pattern container proxy1 proxy2`
|
||
*
|
||
* To assign a proxy and open in no container, use "firefox-default" or "none" as a container name.
|
||
* See also:
|
||
* - [[proxyadd]]
|
||
* - [[proxyremove]]
|
||
*
|
||
* @param args a regex pattern to match URLs followed by the container to open the URL in followed by an optional space-separated list of proxy names.
|
||
*/
|
||
//#background
|
||
export function autocontain(...args: string[]) {
|
||
if (args.length === 0) throw new Error("Invalid autocontain arguments.")
|
||
|
||
const urlMode = args[0] === "-u"
|
||
const saneMode = args[0] === "-s"
|
||
if (urlMode || saneMode) {
|
||
args.splice(0, 1)
|
||
}
|
||
if (args.length < 2) throw new Error("syntax: autocontain [-{u,s}] pattern container proxy1 proxy2")
|
||
|
||
let [pattern, container, ...proxies] = args
|
||
|
||
if (!urlMode) {
|
||
pattern = saneMode ? `^https?://([^/]*\\.|)${pattern}/` : `^https?://[^/]*${pattern}/`
|
||
}
|
||
|
||
proxies.length && Proxy.exists(proxies)
|
||
|
||
return config.set("autocontain", pattern, proxies.length ? [container, proxies.join(",")].join("+") : container)
|
||
}
|
||
|
||
/** Add a proxy for use with [[autocontain]] or `:set proxy`
|
||
|
||
@param name The name of the proxy you want to set
|
||
|
||
@param url The proxy URL. List of supported protcols are "http", "https" or equivalently "ssl", "socks5" or equivalently "socks" and "socks4".
|
||
|
||
Examples:
|
||
- `proxyadd work https://admin:hunter2@bigcorp.example:1337`
|
||
- `proxyadd kyoto socks://10.0.100.10:1080?proxyDNS=false`
|
||
- `proxyadd alice socks4://10.0.100.10:3128`
|
||
|
||
These proxy settings are used by autocontainers. See [[autocontain]]
|
||
*/
|
||
//#background
|
||
export function proxyadd(name: string, url: string) {
|
||
if (!name || !url) throw new Error(":proxyadd requires two arguments. See `:help proxyadd` for more information.")
|
||
|
||
Proxy.proxyFromUrl(url)
|
||
|
||
return config.set("proxies", name, url)
|
||
}
|
||
|
||
/** Remove proxies.
|
||
@param name The proxy name that should be removed.
|
||
*/
|
||
//#background
|
||
export function proxyremove(name: string) {
|
||
if (!name) {
|
||
throw new Error("proxyremove syntax: `proxyremove proxyname`")
|
||
}
|
||
config.unset("proxies", name)
|
||
}
|
||
|
||
/** Remove autocmds
|
||
@param event An event from [[autocmd]]
|
||
|
||
@param url Exactly the "url" you entered when you made the [[autocmd]] you wish to delete. See `:viewconfig autocmds` if you have forgotten.
|
||
*/
|
||
//#background
|
||
export function autocmddelete(event: string, url: string) {
|
||
if (!getAutocmdEvents().includes(event)) throw new Error(`${event} is not a supported event.`)
|
||
if (webrequests.requestEvents.includes(event)) {
|
||
webrequests.unregisterWebRequestAutocmd(event, url)
|
||
}
|
||
return config.unset("autocmds", event, url)
|
||
}
|
||
|
||
/**
|
||
* Helper function to put Tridactyl into ignore mode on the provided URL.
|
||
*
|
||
* Simply creates a DocStart [[autocmd]] that runs `mode ignore`. NB: ignore mode does have a few keybinds by default - see `:viewconfig ignoremaps`. These can be unbound with, e.g. `:unbind --mode=ignore <C-o>`, or `:unbindurl [url] --mode=ignore <C-o>`.
|
||
*
|
||
* Remove sites from the blacklist with `blacklistremove [url]` or `autocmddelete DocStart [url]`.
|
||
*
|
||
* If you're looking for a way to temporarily disable Tridactyl, this might be what you're looking for. If you need to disable Tridactyl more thoroughly on a page look at `:help superignore` instead.
|
||
*
|
||
* <!-- this should probably be moved to an ex alias once configuration has better help --!>
|
||
*
|
||
*/
|
||
//#background
|
||
export function blacklistadd(url: string) {
|
||
return autocmd("DocStart", url, "mode ignore")
|
||
}
|
||
|
||
/** Unbind a sequence of keys so that they do nothing at all.
|
||
|
||
See also:
|
||
|
||
- [[bind]]
|
||
- [[reset]]
|
||
*/
|
||
//#background
|
||
export async function unbind(...args: string[]) {
|
||
const args_obj = parse_bind_args(...args)
|
||
if (args_obj.excmd !== "") throw new Error("unbind syntax: `unbind key`")
|
||
if (args_obj.mode == "browser") {
|
||
const commands = await browser.commands.getAll()
|
||
|
||
const command = commands.filter(c => mozMapToMinimalKey(c.shortcut).toMapstr() == args_obj.key)[0]
|
||
|
||
// Fail quietly if bind doesn't exist so people can safely run it in their RC files
|
||
if (command !== undefined) {
|
||
await browser.commands.update({ name: command.name, shortcut: "" })
|
||
await commandsHelper.updateListener()
|
||
}
|
||
}
|
||
|
||
return config.set(args_obj.configName, args_obj.key, null)
|
||
}
|
||
|
||
/**
|
||
* Unbind a sequence of keys you have set with [[bindurl]]. Note that this **kills** a bind, which means Tridactyl will pass it to the page on `pattern`. If instead you want to use the default setting again, use [[reseturl]].
|
||
*
|
||
* @param pattern a regex to match URLs on which the key should be unbound
|
||
* @param mode Optional. The mode in which the key should be unbound. Defaults to normal.
|
||
* @param keys The keybinding that should be unbound
|
||
*
|
||
* example: `unbindurl jupyter --mode=ignore I`
|
||
*
|
||
* 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]], [[setmode]], [[unsetmode]]
|
||
*/
|
||
//#background
|
||
export async function unbindurl(pattern: string, mode: string, keys: string) {
|
||
const args_obj = parse_bind_args(mode, keys)
|
||
|
||
return config.setURL(pattern, args_obj.configName, args_obj.key, null)
|
||
}
|
||
|
||
/**
|
||
* Restores a sequence of keys to their default value.
|
||
*
|
||
* @param mode Optional. The mode the key should be reset in. Defaults to normal.
|
||
* @param key The key that should be reset.
|
||
*
|
||
* See also:
|
||
* - [[bind]]
|
||
* - [[unbind]]
|
||
*/
|
||
//#background
|
||
export async function reset(mode: string, key: string) {
|
||
const args_obj = parse_bind_args(mode, key)
|
||
return config.unset(args_obj.configName, args_obj.key)
|
||
}
|
||
|
||
/**
|
||
* Restores a sequence of keys to their value in the global config for a specific URL pattern.
|
||
*
|
||
* See also:
|
||
* - [[bind]]
|
||
* - [[unbind]]
|
||
* - [[reset]]
|
||
* - [[bindurl]]
|
||
* - [[unbindurl]]
|
||
* - [[seturl]]
|
||
* - [[unseturl]]
|
||
* - [[setmode]]
|
||
* - [[unsetmode]]
|
||
*/
|
||
//#background
|
||
export async function reseturl(pattern: string, mode: string, key: string) {
|
||
const args_obj = parse_bind_args(mode, key)
|
||
return config.unsetURL(pattern, args_obj.configName, args_obj.key)
|
||
}
|
||
|
||
/** 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
|
||
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).
|
||
These arguments aren't affected by the timespan parameter.
|
||
|
||
Timespan parameter:
|
||
-t [0-9]+(m|h|d|w)
|
||
|
||
Examples:
|
||
|
||
- `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.
|
||
|
||
*/
|
||
//#background
|
||
export async function sanitise(...args: string[]) {
|
||
const flagpos = args.indexOf("-t")
|
||
let since = {}
|
||
// If the -t flag has been given and there is an arg after it
|
||
if (flagpos > -1) {
|
||
if (flagpos < args.length - 1) {
|
||
const match = /^([0-9])+(m|h|d|w)$/.exec(args[flagpos + 1])
|
||
// If the arg of the flag matches Pentadactyl's sanitisetimespan format
|
||
if (match !== null && match.length === 3) {
|
||
// Compute the timespan in milliseconds and get a Date object
|
||
let millis = parseInt(match[1], 10) * 1000
|
||
switch (match[2]) {
|
||
case "w":
|
||
millis *= 7
|
||
case "d":
|
||
millis *= 24
|
||
case "h":
|
||
millis *= 60
|
||
case "m":
|
||
millis *= 60
|
||
}
|
||
since = { since: new Date().getTime() - millis }
|
||
} else {
|
||
throw new Error(":sanitise error: expected time format: ^([0-9])+(m|h|d|w)$, given format:" + args[flagpos + 1])
|
||
}
|
||
} else {
|
||
throw new Error(":sanitise error: -t given but no following arguments")
|
||
}
|
||
}
|
||
|
||
const dts = {
|
||
cache: false,
|
||
cookies: false,
|
||
downloads: false,
|
||
formData: false,
|
||
history: false,
|
||
localStorage: false,
|
||
passwords: false,
|
||
serviceWorkers: false,
|
||
// These are Tridactyl-specific
|
||
commandline: false,
|
||
tridactyllocal: false,
|
||
tridactylsync: false,
|
||
/* When this one is activated, a lot of errors seem to pop up in
|
||
the console. Keeping it disabled is probably a good idea.
|
||
"pluginData": false,
|
||
*/
|
||
/* These 3 are supported by Chrome and Opera but not by Firefox yet.
|
||
"fileSystems": false,
|
||
"indexedDB": false,
|
||
"serverBoundCertificates": false,
|
||
*/
|
||
}
|
||
if (args.find(x => x === "all") !== undefined) {
|
||
for (const attr in dts) if (Object.prototype.hasOwnProperty.call(dts, attr)) dts[attr] = true
|
||
} else {
|
||
// We bother checking if dts[x] is false because
|
||
// browser.browsingData.remove() is very strict on the format of the
|
||
// object it expects
|
||
args.forEach(x => {
|
||
if (dts[x] === false) dts[x] = true
|
||
})
|
||
}
|
||
// Tridactyl-specific items
|
||
if (dts.commandline === true) state.cmdHistory = []
|
||
delete dts.commandline
|
||
if (dts.tridactyllocal === true) await browser.storage.local.clear()
|
||
delete dts.tridactyllocal
|
||
if (dts.tridactylsync === true) await browser.storage.sync.clear()
|
||
delete dts.tridactylsync
|
||
// Global items
|
||
return browser.browsingData.remove(since, dts)
|
||
}
|
||
|
||
/** Bind a quickmark for the current URL or space-separated list of URLs to a key on the keyboard.
|
||
|
||
Afterwards use go[key], gn[key], or gw[key] to [[open]], [[tabopen]], or
|
||
[[winopen]] the URL respectively.
|
||
|
||
Example:
|
||
- `quickmark m https://mail.google.com/mail/u/0/#inbox`
|
||
|
||
*/
|
||
//#background
|
||
export async function quickmark(key: string, ...addressarr: string[]) {
|
||
// ensure we're binding to a single key
|
||
if (key.length !== 1) {
|
||
return
|
||
}
|
||
|
||
if (addressarr.length <= 1) {
|
||
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)
|
||
}
|
||
}
|
||
|
||
/** Puts the contents of config value with keys `keys` into the commandline and the background page console
|
||
|
||
It's a bit rubbish, but we don't have a good way to provide feedback to the commandline yet.
|
||
|
||
You can view the log entry in the browser console (Ctrl-Shift-j).
|
||
|
||
For example, you might try `get nmaps` to see all of your current binds.
|
||
*/
|
||
//#background
|
||
export function get(...keys: string[]) {
|
||
const target = keys.join(".").split(".")
|
||
const value = config.getDynamic(...target)
|
||
console.log(value)
|
||
let done
|
||
if (typeof value === "object") {
|
||
done = fillcmdline_notrail(`# ${keys.join(".")} ${JSON.stringify(value)}`)
|
||
} else {
|
||
done = fillcmdline_notrail(`# ${keys.join(".")} ${value}`)
|
||
}
|
||
return done
|
||
}
|
||
|
||
/**
|
||
* Opens the current configuration in Firefox's native JSON viewer in a new tab.
|
||
*
|
||
* @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.
|
||
*
|
||
* NB: the configuration won't update if you refresh the page - you need to run `:viewconfig` again.
|
||
*
|
||
*/
|
||
//#background
|
||
export function viewconfig(...key: string[]) {
|
||
// # and white space don't agree with FF's JSON viewer.
|
||
// Probably other symbols too.
|
||
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[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("."))
|
||
}
|
||
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}";` })
|
||
}
|
||
|
||
/**
|
||
* Reset a site-specific setting.
|
||
*
|
||
* usage: `unseturl [pattern] key`
|
||
*
|
||
* @param pattern The pattern the setting should be unset on, e.g. `.*wiki.*`. Defaults to the current url.
|
||
* @param key The key that should be unset.
|
||
*
|
||
* Example: `unseturl youtube.com gimode`
|
||
*
|
||
* Note that this removes a setting from the site-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 site-specific setting, using `unseturl` will result in the setting still being set to `false`.
|
||
*
|
||
* Also note that `pattern` should match exactly the one that was used when using `seturl`.
|
||
*/
|
||
//#content
|
||
export function unseturl(pattern: string, key: string) {
|
||
if (!key) {
|
||
key = pattern
|
||
pattern = window.location.href
|
||
}
|
||
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
|
||
*/
|
||
//#background
|
||
export function unset(...keys: string[]) {
|
||
const target = keys.join(".").split(".")
|
||
if (target === undefined) throw new Error("You must define a target!")
|
||
return config.unset(...target)
|
||
}
|
||
|
||
/**
|
||
* "Delete" a default setting. E.g. `setnull searchurls.github` means `open github test` would search your default search engine for "github test".
|
||
*/
|
||
//#background
|
||
export function setnull(...keys: string[]) {
|
||
const target = keys.join(".").split(".")
|
||
if (target === undefined) throw new Error("You must define a target!")
|
||
return config.set(...target, null)
|
||
}
|
||
|
||
// }}}
|
||
|
||
/**
|
||
* @hidden
|
||
*/
|
||
//#content_helper
|
||
const KILL_STACK: Element[] = []
|
||
// {{{ HINTMODE
|
||
|
||
/** Hint a page.
|
||
|
||
@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
|
||
- -p copy an element's text to the clipboard
|
||
- -h select an element (as if you click-n-dragged over it)
|
||
- -P copy an element's title/alt text to the clipboard
|
||
- -r read an element's text with text-to-speech
|
||
- -i view an image
|
||
- -I view an image in a new tab
|
||
- -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
|
||
- -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 <Esc>), 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.
|
||
- -F [callback] - run a custom callback on the selected hint, e.g. `hint -JF e => {tri.excmds.tabopen("-b",e.href); e.remove()}`.
|
||
|
||
Element selection flags:
|
||
- -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]`
|
||
- -C [selector] like `-c [selector]` but also hints all elements that would normally be hinted given the other options selected
|
||
- -x [selector] exclude the matched elements from hinting
|
||
- -f [text] hint links and inputs that display the given text
|
||
- `bind <c-e> hint -f Edit`
|
||
- Backslashes can escape spaces: `bind <c-s> hint -f Save\ as`
|
||
- -fr [text] use RegExp to hint the links and inputs
|
||
- -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
|
||
- -V create hints for invisible elements. By default, elements outside the viewport when calling :hint are not hinted, this includes them anyways.
|
||
|
||
Hinting mode selection:
|
||
- -q* quick (or rapid) hints mode. Stay in hint mode until you press <Esc>, e.g. `:hint -qb` to open multiple hints in the background or `:hint -qW excmd` to execute excmd once for each hint. This will return an array containing all elements or the result of executed functions (e.g. `hint -qpipe a href` will return an array of links).
|
||
- 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
|
||
- -! execute all hints without waiting for a selection
|
||
- For example, `hint -!bf Comments` opens in background tabs all visible links whose text matches `Comments`
|
||
|
||
Deprecated options:
|
||
- -br deprecated, use `-qb` instead
|
||
|
||
Excepting the custom selector mode, background hint mode and the "immediate" modifier, each of these hint modes is available by default as `;<option character>`, so e.g. `;y` to yank a link's target; `;g<option character>` starts rapid hint mode for all modes where it makes sense, and some others.
|
||
|
||
To open a hint in the background, the default bind is `F`.
|
||
|
||
Ex-commands available exclusively in hint mode are listed [here](/static/docs/modules/_src_content_hinting_.html)
|
||
|
||
Related settings:
|
||
- "hintchars": "hjklasdfgyuiopqwertnmzxcvb"
|
||
- "hintfiltermode": "simple" | "vimperator" | "vimperator-reflow"
|
||
- "relatedopenpos": "related" | "next" | "last"
|
||
- "hintuppercase": "true" | "false"
|
||
- "hintnames": "short" | "uniform" | "numeric"
|
||
- "hintdelay": 300
|
||
- "hintshift": "true" | "false"
|
||
- "hintautoselect": "true" | "false"
|
||
|
||
With "short" names, Tridactyl will generate short hints that
|
||
are never prefixes of each other. With "uniform", Tridactyl
|
||
will generate hints of uniform length. In either case, the
|
||
hints are generated from the set in "hintchars".
|
||
|
||
With "numeric" names, hints are always assigned using
|
||
sequential integers, and "hintchars" is ignored. This has the
|
||
disadvantage that some hints are prefixes of others (and you
|
||
need to hit space or enter to select such a hint). But it has
|
||
the advantage that the hints tend to be more predictable
|
||
(e.g., a news site will have the same hints for its
|
||
boilerplate each time you visit it, even if the number of
|
||
links in the main body changes).
|
||
|
||
There are some extra hint "modes" that are actually just normal-mode binds. We'll list them here:
|
||
|
||
- `;gv` - "open link in MPV" - only available if you have [[native]] installed and `mpv` on your PATH
|
||
- `;m` and `;M` - do a reverse image search using Google in the current tab and a new tab
|
||
- `;x` and `;X` - move cursor to element and perform a real click or ctrl-shift-click (to open in a new foreground tab). Only available on Linux, if you have [[native]] installed and `xdotool` on your PATH
|
||
- `;d` and `;gd` - open links in discarded background tabs (defer loading until tab is switched to)
|
||
|
||
NB: by default, hinting respects whether links say they should be opened in new tabs (i.e. `target=_blank`). If you wish to override this you can use `:hint -JW open` to force the hints to open in the current tab. JavaScript hints (grey ones) will always open wherever they want, but if you want to include these anyway you can use `:hint -W open`.
|
||
|
||
*/
|
||
//#content
|
||
export async function hint(...args: string[]): Promise<any> {
|
||
// Parse configuration and print parsing warnings
|
||
const config = hint_util.HintConfig.parse(args)
|
||
config.printWarnings(logger)
|
||
|
||
const hintTabOpen = async (href, active = !config.rapid) => {
|
||
const containerId = await activeTabContainerId()
|
||
if (containerId) {
|
||
return openInNewTab(href, {
|
||
active,
|
||
related: true,
|
||
cookieStoreId: containerId,
|
||
})
|
||
} else {
|
||
return openInNewTab(href, {
|
||
active,
|
||
related: true,
|
||
})
|
||
}
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const hintables = config.hintables()
|
||
|
||
// If the user specified a callback, eval it, else use the default
|
||
// action which performs the action matching the open mode
|
||
const action = config.callback
|
||
? eval(config.callback)
|
||
: (elem: any) => {
|
||
if (config.pipeAttribute !== null) {
|
||
// We have an attribute to pipe
|
||
return elem[config.pipeAttribute]
|
||
}
|
||
|
||
if (config.excmd) {
|
||
// We have an excmd to run. By spec, we append the element's href
|
||
if (elem.href) {
|
||
// /!\ RACY RACY RACY!
|
||
run_exstr(config.excmd + " " + elem.href)
|
||
return elem
|
||
}
|
||
|
||
// Otherwise, no href so nothing to do
|
||
return
|
||
}
|
||
|
||
switch (config.openMode) {
|
||
case OpenMode.Highlight:
|
||
const r = document.createRange()
|
||
r.setStart(elem, 0)
|
||
r.setEnd(elem, 1)
|
||
const s = document.getSelection()
|
||
s.addRange(r)
|
||
return elem
|
||
|
||
case OpenMode.Images:
|
||
case OpenMode.ImagesTab:
|
||
const src = elem.getAttribute("src")
|
||
if (src) {
|
||
if (config.openMode === OpenMode.ImagesTab) {
|
||
// TODO: await? Other hintTabOpen calls don't seem to use one
|
||
hintTabOpen(new URL(src, window.location.href).href)
|
||
} else {
|
||
open(new URL(src, window.location.href).href)
|
||
}
|
||
return elem
|
||
}
|
||
|
||
return
|
||
|
||
case OpenMode.Kill:
|
||
elem.remove()
|
||
return elem
|
||
|
||
case OpenMode.KillTridactyl:
|
||
elem.classList.add("TridactylKilledElem")
|
||
KILL_STACK.push(elem)
|
||
return elem
|
||
|
||
case OpenMode.SaveResource:
|
||
case OpenMode.SaveImage:
|
||
case OpenMode.SaveAsResource:
|
||
case OpenMode.SaveAsImage:
|
||
const saveAs = config.openMode === OpenMode.SaveAsResource || config.openMode === OpenMode.SaveAsImage
|
||
const attr = config.openMode === OpenMode.SaveImage || config.openMode === OpenMode.SaveAsImage ? "src" : "href"
|
||
Messaging.message("download_background", "downloadUrl", new URL(elem[attr], window.location.href).href, saveAs)
|
||
return elem
|
||
|
||
case OpenMode.Scroll:
|
||
elem.scrollIntoView(true)
|
||
return elem
|
||
|
||
case OpenMode.ScrollFocus:
|
||
let tabindexAdded = false
|
||
// img can only be focused when they have the tabindex attribute
|
||
if (elem instanceof HTMLImageElement && !elem.getAttribute("tabindex")) {
|
||
elem.setAttribute("tabindex", "-1")
|
||
tabindexAdded = true
|
||
}
|
||
elem.focus()
|
||
scrolling.setCurrentFocus(elem)
|
||
// img doesn't get unfocused when its tabindex is removed, so no need to keep it around
|
||
if (tabindexAdded) elem.removeAttribute("tabindex")
|
||
return elem
|
||
|
||
case OpenMode.TTSRead:
|
||
TTS.readText(elem.textContent)
|
||
return elem
|
||
|
||
case OpenMode.YankAlt:
|
||
// Yank link alt text
|
||
// ???: Neither anchors nor links posses an "alt" attribute. I'm assuming that the person who wrote this code also wanted to select the alt text of images
|
||
return elem.title ? elem.title : elem.alt
|
||
|
||
case OpenMode.YankAnchor:
|
||
const anchorUrl = new URL(window.location.href)
|
||
// ???: What purpose does selecting elements with a name attribute have? Selecting values that only have meaning in forms doesn't seem very useful.
|
||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
||
anchorUrl.hash = elem.id || elem.name
|
||
return anchorUrl.href
|
||
|
||
case OpenMode.YankLink:
|
||
if (elem.href) {
|
||
return elem.href
|
||
}
|
||
|
||
return
|
||
|
||
case OpenMode.YankText:
|
||
return elem.textContent
|
||
}
|
||
|
||
if (elem.href) {
|
||
elem.focus()
|
||
|
||
switch (config.openMode) {
|
||
case OpenMode.Default:
|
||
DOM.simulateClick(elem)
|
||
break
|
||
case OpenMode.Tab:
|
||
hintTabOpen(elem.href, true).catch(() => DOM.simulateClick(elem, DOM.TabTarget.NewTab))
|
||
break
|
||
case OpenMode.BackgroundTab:
|
||
hintTabOpen(elem.href, false).catch(() => DOM.simulateClick(elem, DOM.TabTarget.NewBackgroundTab))
|
||
break
|
||
case OpenMode.Window:
|
||
openInNewWindow({ url: new URL(elem.href, window.location.href).href })
|
||
break
|
||
case OpenMode.WindowPrivate:
|
||
openInNewWindow({ url: elem.href, incognito: true })
|
||
break
|
||
}
|
||
} else {
|
||
if (config.openMode === OpenMode.WindowPrivate) {
|
||
// We want a private window, but the element doesn't have an href, so
|
||
// we avoid opening the target by accident
|
||
return
|
||
} else {
|
||
elem.focus()
|
||
DOM.simulateClick(elem)
|
||
}
|
||
}
|
||
|
||
return elem
|
||
}
|
||
|
||
if (config.immediate) {
|
||
// Immediate mode, perform the target action on all matching nodes
|
||
const results = []
|
||
|
||
for (const elements of hintables) {
|
||
for (const hintable of elements.elements) {
|
||
try {
|
||
results.push(action(hintable))
|
||
} catch (error) {
|
||
logger.error(error)
|
||
}
|
||
}
|
||
}
|
||
|
||
resolve(results)
|
||
} else {
|
||
// Perform hinting
|
||
hinting.hintPage(hintables, action, resolve, reject, config.rapid)
|
||
}
|
||
}).then(value => {
|
||
// Fix #1374 for all types of yanks: join returned results
|
||
if (config.isYank) {
|
||
if (Array.isArray(value)) {
|
||
yank(value.join("\n"))
|
||
} else {
|
||
yank(value as string)
|
||
}
|
||
}
|
||
|
||
return value
|
||
})
|
||
}
|
||
|
||
// how 2 crash pc
|
||
////#content
|
||
//export async function rapid(...commands: string[]){
|
||
// while(true){
|
||
// await run_exstr(...commands)
|
||
// }
|
||
//}
|
||
|
||
/**
|
||
* Perform rot13.
|
||
*
|
||
* Transforms all text nodes in the current tab via rot13. Only characters in
|
||
* the ASCII range are considered.
|
||
*
|
||
* @param n number of characters to shift.
|
||
*/
|
||
//#content
|
||
export function rot13(n: number) {
|
||
if (n === undefined) n = 13
|
||
const body = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: () => NodeFilter.FILTER_ACCEPT })
|
||
|
||
while (body.nextNode()) {
|
||
const t = body.currentNode.textContent
|
||
body.currentNode.textContent = rot13_helper(t, n)
|
||
}
|
||
}
|
||
/**
|
||
* Perform text jumbling (reibadailty).
|
||
*
|
||
* Shuffles letters except for first and last in all words in text nodes in the current tab. Only characters in
|
||
* the ASCII range are considered.
|
||
*
|
||
* Inspired by: https://www.newscientist.com/letter/mg16221887-600-reibadailty/
|
||
*/
|
||
//#content
|
||
export function jumble() {
|
||
const body = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: () => NodeFilter.FILTER_ACCEPT })
|
||
|
||
while (body.nextNode()) {
|
||
const t = body.currentNode.textContent
|
||
body.currentNode.textContent = jumble_helper(t)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Hacky ex string parser.
|
||
*
|
||
* Use it for fire-and-forget running of background commands in content.
|
||
*/
|
||
//#content
|
||
export function run_exstr(...commands: string[]) {
|
||
return Messaging.message("controller_background", "acceptExCmd", commands.join(""))
|
||
}
|
||
|
||
// }}}
|
||
|
||
// {{{ GOBBLE mode
|
||
|
||
/** Initialize gobble mode.
|
||
|
||
If numKeysOrTerminator is a number, it will read the provided amount of keys;
|
||
If numKeysOrTerminator is a key or key combination like 'k', '<CR>' or '<C-j>';
|
||
it will read keys until the provided key is pressed.
|
||
Then it will append the keypresses to `endCmd` and execute that string,
|
||
also appending arguments if provided.
|
||
*/
|
||
//#content
|
||
export async function gobble(numKeysOrTerminator: string, endCmd: string, ...args: string[]) {
|
||
return gobbleMode.init(numKeysOrTerminator, endCmd, ...args)
|
||
}
|
||
|
||
// }}}
|
||
|
||
/** @hidden
|
||
* This function is used by goto completions.
|
||
*/
|
||
//#content
|
||
export async function getGotoSelectors(): Promise<Array<{ level: number; y: number; title: string; selector: string }>> {
|
||
const result = []
|
||
let level = 1
|
||
for (const selector of config.get("gotoselector").split(",")) {
|
||
result.push(
|
||
...(Array.from(document.querySelectorAll(selector)) as HTMLElement[])
|
||
.filter(e => e.innerText)
|
||
.map(e => ({ level, y: e.getClientRects()[0]?.y, title: e.innerText, selector: DOM.getSelector(e) }))
|
||
.filter(e => e.y !== undefined),
|
||
)
|
||
level += 1
|
||
}
|
||
return result
|
||
}
|
||
|
||
/**
|
||
* Jump to selector.
|
||
*/
|
||
//#content
|
||
export async function goto(...selector: string[]) {
|
||
const element = document.querySelector(selector.join(" "))
|
||
if (element) {
|
||
element.scrollIntoView()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Initialize n [mode] mode.
|
||
*
|
||
* In this special mode, a series of key sequences are executed as bindings from a different mode, as specified by the
|
||
* `mode` argument. After the count of accepted sequences is `n`, the finalizing ex command given as the `endexArr`
|
||
* argument is executed, which defaults to `mode ignore`.
|
||
*
|
||
* Example: `:nmode normal 1 mode ignore`
|
||
* This looks up the next key sequence in the normal mode bindings, executes it, and switches the mode to `ignore`.
|
||
* If the key sequence does not match a binding, it will be silently passed through to Firefox, but it will be counted
|
||
* for the termination condition.
|
||
*/
|
||
//#content
|
||
export async function nmode(mode: string, n: number, ...endexArr: string[]) {
|
||
const endex = endexArr.join(" ") || "mode ignore"
|
||
return nMode.init(endex, mode, n)
|
||
}
|
||
|
||
// {{{TEXT TO SPEECH
|
||
|
||
/**
|
||
* Read text content of elements matching the given selector
|
||
*
|
||
* @param selector the selector to match elements
|
||
*/
|
||
//#content_helper
|
||
function tssReadFromCss(selector: string): void {
|
||
const elems = DOM.getElemsBySelector(selector, [])
|
||
|
||
elems.forEach(e => {
|
||
TTS.readText(e.textContent)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Read the given text using the browser's text to speech functionality and
|
||
* the settings currently set
|
||
*
|
||
* @param mode the command mode
|
||
* -t read the following args as text
|
||
* -c read the content of elements matching the selector
|
||
*/
|
||
//#content
|
||
export async function ttsread(mode: "-t" | "-c", ...args: string[]) {
|
||
if (mode === "-t") {
|
||
// really should quote args, but for now, join
|
||
TTS.readText(args.join(" "))
|
||
} else if (mode === "-c") {
|
||
if (args.length > 0) {
|
||
tssReadFromCss(args[0])
|
||
} else {
|
||
throw new Error("Error: no CSS selector supplied")
|
||
}
|
||
} else {
|
||
throw new Error("Unknown mode for ttsread command: " + mode)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show a list of the voices available to the TTS system. These can be
|
||
* set in the config using `ttsvoice`
|
||
*/
|
||
//#background
|
||
export async function ttsvoices() {
|
||
const voices = TTS.listVoices()
|
||
voices.sort()
|
||
// need a better way to show this to the user
|
||
return fillcmdline_notrail("#", voices.join(", "))
|
||
}
|
||
|
||
/**
|
||
* Cancel current reading and clear pending queue
|
||
*
|
||
* Arguments:
|
||
* - stop: cancel current and pending utterances
|
||
*/
|
||
//#content
|
||
export async function ttscontrol(action: string) {
|
||
// only pause seems to be working, so only provide access to that
|
||
// to avoid exposing users to things that won't work
|
||
if (action !== "stop") {
|
||
throw new Error("Unknown text-to-speech action: " + action)
|
||
}
|
||
|
||
return TTS.doAction(action as TTS.Action)
|
||
}
|
||
|
||
//}}}
|
||
|
||
// {{{ PERFORMANCE LOGGING
|
||
|
||
/**
|
||
* Build a set of FilterConfigs from a list of human-input filter
|
||
* specs.
|
||
*
|
||
* @hidden
|
||
*/
|
||
//#background_helper
|
||
export function buildFilterConfigs(filters: string[]): Perf.StatsFilterConfig[] {
|
||
return filters.map((filter: string): Perf.StatsFilterConfig => {
|
||
if (filter.endsWith("/")) {
|
||
return { kind: "ownerName", ownerName: filter.slice(0, -1) }
|
||
} else if (filter === ":start") {
|
||
return { kind: "eventType", eventType: "start" }
|
||
} else if (filter === ":end") {
|
||
return { kind: "eventType", eventType: "end" }
|
||
} else if (filter === ":measure") {
|
||
return { kind: "eventType", eventType: "measure" }
|
||
} else {
|
||
// This used to say `functionName: name`
|
||
// which didn't seem to exist anywhere
|
||
//
|
||
// So at least we return something now
|
||
return { kind: "functionName", functionName: filter }
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Dump the raw json for our performance counters. Filters with
|
||
* trailing slashes are class names, :start | :end | :measure specify
|
||
* what type of sample to pass through, and all others are function
|
||
* names. All filters must match for a sample to be dumped.
|
||
*
|
||
* Tridactyl does not collect performance information by default. To
|
||
* get this data you'll have to set the configuration option
|
||
* `perfcounters` to `"true"`. You may also want to examine the value
|
||
* of `perfsamples`.
|
||
*/
|
||
//#background
|
||
export async function perfdump(...filters: string[]) {
|
||
const filterconfigs = buildFilterConfigs(filters)
|
||
const entries = window.tri.statsLogger.getEntries(...filterconfigs)
|
||
console.log(filterconfigs)
|
||
return open("data:application/json;charset=UTF-8," + JSON.stringify(entries))
|
||
}
|
||
|
||
/**
|
||
* Pretty-print a histogram of execution durations for you. Arguments
|
||
* are as above, with the addition that this automatically filters to
|
||
* counter samples of type :measure.
|
||
*
|
||
* Note that this will display its output by opening a data: url with
|
||
* text in the place of your current tab.
|
||
*/
|
||
//#background
|
||
export async function perfhistogram(...filters: string[]) {
|
||
const filterconfigs = buildFilterConfigs(filters)
|
||
filterconfigs.push({ kind: "eventType", eventType: "measure" })
|
||
const entries = window.tri.statsLogger.getEntries(...filterconfigs)
|
||
if (entries.length === 0) {
|
||
return fillcmdline_tmp(3000, "perfhistogram: No samples found.")
|
||
}
|
||
const histogram = Perf.renderStatsHistogram(entries)
|
||
console.log(histogram)
|
||
return open("data:text/plain;charset=UTF-8;base64," + btoa(histogram))
|
||
}
|
||
|
||
// }}}
|
||
|
||
// unsupported on android
|
||
/**
|
||
* Add or remove a bookmark.
|
||
*
|
||
* Optionally, you may give the bookmark a title. If no URL is given, a bookmark is added for the current page.
|
||
*
|
||
* If a bookmark already exists for the URL, it is removed, even if a title is given.
|
||
*
|
||
* Does not support creation of folders: you'll need to use the Firefox menus for that.
|
||
*
|
||
* @param titlearr Title for the bookmark (can include spaces but not forward slashes, as these are interpreted as folders). If you want to put the bookmark in a folder, you can:
|
||
* - Specify it exactly: `/Bookmarks Menu/Mozilla Firefox/My New Bookmark Title`
|
||
* - Specify it by a subset: `Firefox/My New Bookmark Title`
|
||
* - and leave out the title if you want: `Firefox/`
|
||
*/
|
||
//#background
|
||
export async function bmark(url?: string, ...titlearr: string[]) {
|
||
const auto_url = url == undefined || url == (await activeTab()).url
|
||
url =
|
||
url === undefined
|
||
? (await activeTab()).url
|
||
: (_ => {
|
||
try {
|
||
return new URL(url).href
|
||
} catch (e) {
|
||
return new URL("http://" + url).href
|
||
}
|
||
})()
|
||
let title = titlearr.join(" ")
|
||
// if titlearr is given and we have duplicates, we probably want to give an error here.
|
||
const dupbmarks = await browser.bookmarks.search({ url })
|
||
dupbmarks.forEach(bookmark => browser.bookmarks.remove(bookmark.id))
|
||
if (dupbmarks.length !== 0) return
|
||
const path = title.substring(0, title.lastIndexOf("/") + 1)
|
||
// if title is blank, get it from the current page.
|
||
// technically could race condition if someone switched tabs REALLY quick after
|
||
// bookmarking, but too unlikely to bother with for now
|
||
if (title == "" && auto_url) {
|
||
//retrieve title from current tab
|
||
title = (await activeTab()).title
|
||
}
|
||
|
||
if (path != "") {
|
||
const tree = (await browser.bookmarks.getTree())[0] // Why would getTree return a tree? Obviously it returns an array of unit length.
|
||
// I hate recursion.
|
||
const treeClimber = (tree: browser.bookmarks.BookmarkTreeNode, treestr) => {
|
||
if (tree.type !== "folder") return {}
|
||
treestr += tree.title + "/"
|
||
if (!("children" in tree) || tree.children.length === 0) return [{ path: treestr, id: tree.id }]
|
||
return [{ path: treestr, id: tree.id }, tree.children.map(child => treeClimber(child, treestr))]
|
||
}
|
||
const treeClimberResult = treeClimber(tree, "")
|
||
let validpaths = []
|
||
if (treeClimberResult instanceof Array) validpaths = treeClimberResult.flat(Infinity).filter(x => "path" in x)
|
||
title = title.substring(title.lastIndexOf("/") + 1)
|
||
let pathobj = validpaths.find(p => p.path === path)
|
||
// If strict look doesn't find it, be a bit gentler
|
||
if (pathobj === undefined) pathobj = validpaths.find(p => p.path.includes(path))
|
||
//technically an initial title string like `Firefox/` can give us a blank title
|
||
//once we remove the path, so let's fix that
|
||
if (title == "" && auto_url) {
|
||
//retrieve title from current tab
|
||
const currTitle = (await activeTab()).title
|
||
title = currTitle
|
||
}
|
||
|
||
if (pathobj !== undefined) {
|
||
return browser.bookmarks.create({ url, title, parentId: pathobj.id })
|
||
} // otherwise, give the user an error, probably with [v.path for v in validpaths]
|
||
}
|
||
|
||
return browser.bookmarks.create({ url, title })
|
||
}
|
||
|
||
//#background
|
||
export function echo(...str: string[]) {
|
||
return str.join(" ")
|
||
}
|
||
|
||
/** helper function for js and jsb
|
||
*
|
||
* -p to take a single extra argument located at the end of str[]
|
||
* -d[delimiter character] to take a space-separated array of arguments after the delimiter
|
||
* -s to load js script of a source file from the config path
|
||
*
|
||
* @hidden
|
||
*/
|
||
async function js_helper(str: string[]) {
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
|
||
let JS_ARG = null
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
|
||
let JS_ARGS = []
|
||
let jsContent: string = null
|
||
|
||
let doSource = false
|
||
let fromRC = false
|
||
let separator = null
|
||
|
||
while (true) {
|
||
const flag = str[0]
|
||
|
||
if (flag == "-p") {
|
||
// arg of -p comes from the end of str[]
|
||
// and we don't know if the user will use it or not
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
JS_ARG = str.pop()
|
||
str.shift()
|
||
continue
|
||
}
|
||
|
||
if (flag == "-s") {
|
||
doSource = true
|
||
str.shift()
|
||
continue
|
||
}
|
||
|
||
if (flag == "-r") {
|
||
doSource = true
|
||
fromRC = true
|
||
str.shift()
|
||
continue
|
||
}
|
||
|
||
// d for delimiter innit
|
||
const match = /-d(.)/.exec(flag)
|
||
if (match !== null) {
|
||
separator = match[1]
|
||
str.shift()
|
||
continue
|
||
}
|
||
|
||
break
|
||
}
|
||
|
||
if (separator !== null) {
|
||
// user may or may not use JS_ARGS
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
JS_ARGS = str.join(" ").split(separator)[1].split(" ")
|
||
jsContent = str.join(" ").split(separator)[0]
|
||
} else {
|
||
jsContent = str.join(" ")
|
||
}
|
||
|
||
if (doSource) {
|
||
let sourcePath = jsContent
|
||
if (fromRC) {
|
||
const sep = "/"
|
||
const rcPath = (await Native.getrcpath("unix")).split(sep).slice(0, -1)
|
||
sourcePath = [...rcPath, sourcePath].join(sep)
|
||
}
|
||
const file = await Native.read(sourcePath)
|
||
if (file.code !== 0) throw new Error("Couldn't read js file " + sourcePath)
|
||
jsContent = file.content
|
||
}
|
||
|
||
return eval(jsContent)
|
||
}
|
||
|
||
/**
|
||
* Lets you execute JavaScript in the page context. If you want to get the result back, use
|
||
*
|
||
* `composite js ... | fillcmdline`
|
||
*
|
||
* @returns Last value of the JavaScript statement
|
||
*
|
||
* Usage:
|
||
*
|
||
* `js [-p] javascript code ... [arg]`
|
||
*
|
||
* `js [-s|-r|-p] javascript_filename [arg]`
|
||
*
|
||
* - options
|
||
* - -p pass an argument to js for use with `composite`. The argument is passed as the last space-separated argument of `js`, i.e. `str[str.length-1]` and stored in the magic variable JS_ARG - see below for example usage.
|
||
* -d[delimiter character] to take a space-separated array of arguments after the delimiter, stored in the magic variable `JS_ARGS` (which is an array).
|
||
* - -s load the js source from a Javascript file.
|
||
* - -r load the js source from a Javascript file relative to your RC file. (NB: will throw an error if no RC file exists)
|
||
*
|
||
* Some of Tridactyl's functions are accessible here via the `tri` object. Just do `console.log(tri)` in the web console on the new tab page to see what's available.
|
||
*
|
||
* If you want to pipe an argument to `js`, you need to use the "-p" flag or "-d" flag with an argument and then use the JS_ARG global variable, e.g:
|
||
*
|
||
* `composite get_current_url | js -p alert(JS_ARG)`
|
||
*
|
||
* To run JavaScript from a source file:
|
||
*
|
||
* `js -s ~/JSLib/my_script.js`
|
||
*
|
||
* To run a JavaScript file relative to your RC file, e.g. `~/.config/tridactyl/sample.js`
|
||
*
|
||
* `js -r sample.js`
|
||
*
|
||
* `js` executes JavaScript in local scope. If you want to reuse the code in other :js calls, you can add your functions or variables into a global namespace, like `window.` or `tri.`, e.g.:
|
||
*
|
||
* `js tri.hello = function (){ alert("hello world!") };`
|
||
* `js tri.hello()`
|
||
*
|
||
* You can use `-d` to make your own ex-commands:
|
||
*
|
||
* `command loudecho js -d€ window.alert(JS_ARGS.join(" "))€`
|
||
*
|
||
*/
|
||
/* tslint:disable:no-identical-functions */
|
||
//#content
|
||
export async function js(...str: string[]) {
|
||
return js_helper(str)
|
||
}
|
||
|
||
/**
|
||
* Lets you execute JavaScript in the background context. All the help from [[js]] applies. Gives you a different `tri` object which has access to more excmds and web-extension APIs.
|
||
*
|
||
* In `:jsb`, the `tri` object has a special `tabs` property that can be used to access the window object of the corresponding tab by indexing it with the tab ID. Here are a few examples:
|
||
*
|
||
* - Get the URL of the tab whose id 3:
|
||
* `:jsb tri.tabs[3].location.href.then(console.log)`
|
||
* - Set the title of the tab whose id is 6:
|
||
* `:jsb tri.tabs[3].document.title = "New title!"`
|
||
* - Run `alert()` in all tabs:
|
||
* `:jsb browser.tabs.query({}).then(tabs => tabs.forEach(tab => tri.tabs[tab.id].tri.excmds.js('alert()')))`
|
||
*
|
||
* When fetching a value or running a function in a tab through the `tabs` property, the returned value is a Promise and must be awaited.
|
||
* Setting values through the `tab` property is asynchronous too and there is no way to await this operation.
|
||
* If you need to ensure that the value has been set before performing another action, use tri.tabs[tab.id].tri.excmds.js to set the value instead and await the result.
|
||
*/
|
||
/* tslint:disable:no-identical-functions */
|
||
//#background
|
||
export async function jsb(...str: string[]) {
|
||
return js_helper(str)
|
||
}
|
||
|
||
/**
|
||
* Opens a new tab the url of which is "https://github.com/tridactyl/tridactyl/issues/new" and automatically fill add tridactyl, firefox and os version to the issue.
|
||
*/
|
||
//#content
|
||
export async function issue() {
|
||
const newIssueUrl = "https://github.com/tridactyl/tridactyl/issues/new"
|
||
if (window.location.href !== newIssueUrl) {
|
||
return tabopen(newIssueUrl)
|
||
}
|
||
const textarea = document.getElementById("issue_body")
|
||
if (!(textarea instanceof HTMLTextAreaElement)) {
|
||
logger.warning("issue(): Couldn't find textarea element in github issue page.")
|
||
return
|
||
}
|
||
let template = await fetch(browser.runtime.getURL("issue_template.md"))
|
||
.then(resp => resp.body.getReader())
|
||
.then(reader => reader.read())
|
||
.then(r => new TextDecoder("utf-8").decode(r.value))
|
||
if (textarea.value !== template) {
|
||
logger.debug("issue(): Textarea value differs from template, exiting early.")
|
||
return
|
||
}
|
||
const platform = await browserBg.runtime.getPlatformInfo()
|
||
// Remove the bit asking the user
|
||
template = template.replace("- Operating system:\n", "")
|
||
// Add this piece of information to the top of the template
|
||
template = `Operating system: ${platform.os}\n` + template
|
||
|
||
const info = await browserBg.runtime.getBrowserInfo()
|
||
template = template.replace("- Firefox version (Top right menu > Help > About Firefox):\n\n", "")
|
||
template = `Firefox version: ${info.vendor} ${info.name} ${info.version}\n` + template
|
||
|
||
template = template.replace("- Tridactyl version (`:version`):\n\n", "")
|
||
template = `Tridactyl version: ${TRI_VERSION}\n` + template
|
||
|
||
textarea.value = template
|
||
}
|
||
|
||
/**
|
||
* Generates a QR code for the given text. By default opens in new tab. Default binds close the new tab after 5 seconds.
|
||
* If no text is passed as an argument then it checks if any text is selected and creates a QR code for that.
|
||
* If no selection is found then it creates QR code for the current tab's URL
|
||
*
|
||
* `text2qr --popup [...]` will open the QR code in a new popup window
|
||
*
|
||
* `text2qr --window [...]` will open the QR code in a new window
|
||
*
|
||
* `text2qr --current [...]` will open in the current tab
|
||
*
|
||
* `text2qr --timeout <timeout in seconds> [...]` closes the tab/window/popup after specified number of seconds
|
||
*
|
||
* Example: text2qr --timeout 5 --popup hello world
|
||
*/
|
||
//#content
|
||
export async function text2qr(...args: string[]) {
|
||
let text: string = null
|
||
let isParsed = false
|
||
let openMode = null
|
||
let timeout = "-1"
|
||
while (!isParsed) {
|
||
switch (args[0]) {
|
||
case "--window":
|
||
openMode = winopen
|
||
args.shift()
|
||
break
|
||
case "--popup":
|
||
openMode = (...args) => winopen("-popup", ...args)
|
||
args.shift()
|
||
break
|
||
case "--current":
|
||
openMode = open
|
||
args.shift()
|
||
break
|
||
case "--timeout":
|
||
args.shift()
|
||
timeout = args[0]
|
||
args.shift()
|
||
break
|
||
default:
|
||
isParsed = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!openMode) openMode = tabopen // default to new tab if no option provided
|
||
|
||
text = args.join(" ").trim()
|
||
if (!text || text.length == 0) {
|
||
text = window.location.href
|
||
}
|
||
const urlEncodedText = encodeURIComponent(text)
|
||
const url = new URL(browser.runtime.getURL("static/qrcode.html"))
|
||
url.searchParams.append("data", btoa(urlEncodedText))
|
||
url.searchParams.append("timeout", timeout)
|
||
openMode(url.href)
|
||
}
|
||
|
||
/**
|
||
* Checks if there are any stable updates available for Tridactyl.
|
||
*
|
||
* Related settings:
|
||
*
|
||
* - `update.nag = true | false` - checks for updates on Tridactyl start.
|
||
* - `update.nagwait = 7` - waits 7 days before nagging you to update.
|
||
* - `update.checkintervalsecs = 86400` - waits 24 hours between checking for an update.
|
||
*
|
||
*/
|
||
//#background
|
||
export async function updatecheck(source: "manual" | "auto_polite" | "auto_impolite" = "manual") {
|
||
const forceCheck = source == "manual"
|
||
|
||
// Skip check unless it's due or forced
|
||
if (!(forceCheck || Updates.secondsSinceLastCheck() > config.get("update", "checkintervalsecs"))) {
|
||
return false
|
||
}
|
||
|
||
const highestKnownVersion = await Updates.getLatestVersion()
|
||
if (!highestKnownVersion) {
|
||
return false
|
||
}
|
||
|
||
if (!Updates.shouldNagForVersion(highestKnownVersion)) {
|
||
if (source == "manual") {
|
||
fillcmdline_tmp(30000, "You're up to date! Tridactyl version " + highestKnownVersion.version + ".")
|
||
}
|
||
return false
|
||
}
|
||
|
||
const notify = () => {
|
||
fillcmdline_tmp(30000, "Tridactyl " + highestKnownVersion.version + " is available (you're on " + Updates.getInstalledVersion() + "). Visit about:addons, right click Tridactyl, click 'Find Updates'. Restart Firefox once it has downloaded.")
|
||
}
|
||
|
||
// A bit verbose, but I figured it was important to have the logic
|
||
// right when it comes to automatically nagging users about the
|
||
// version they're on.
|
||
if (source == "manual") {
|
||
notify()
|
||
} else if (source == "auto_impolite") {
|
||
logger.debug("Impolitely nagging user to update. Installed, latest: ", Updates.getInstalledVersion(), highestKnownVersion)
|
||
notify()
|
||
Updates.updateLatestNaggedVersion(highestKnownVersion)
|
||
} else if (source == "auto_polite" && !Updates.naggedForVersion(highestKnownVersion)) {
|
||
logger.debug("Politely nagging user to update. Installed, latest: ", Updates.getInstalledVersion(), highestKnownVersion)
|
||
notify()
|
||
Updates.updateLatestNaggedVersion(highestKnownVersion)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Feed some keys to Tridactyl's parser. E.g. `keyfeed jkjkjkjkjkjkjkjkjkjkjkjkjkjkjkjkjkjkjkjkjkjjkj`.
|
||
*
|
||
* NB:
|
||
*
|
||
* - Does _not_ function like Vim's noremap - `bind j keyfeed j` will cause an infinite loop.
|
||
* - Doesn't work in exmode - i.e. `keyfeed t<CR>` won't work.
|
||
*
|
||
*/
|
||
//#content
|
||
export async function keyfeed(mapstr: string) {
|
||
const keyseq = mapstrToKeyseq(mapstr)
|
||
for (const k of keyseq) {
|
||
KEY_MUNCHER.next(k)
|
||
await sleep(10)
|
||
}
|
||
}
|
||
|
||
/** Open a welcome page on first install.
|
||
*
|
||
* @hidden
|
||
*/
|
||
//#background_helper
|
||
browser.runtime.onInstalled.addListener(details => {
|
||
if (details.reason === "install") tutor("newtab")
|
||
else if (details.reason === "update") {
|
||
if ((details as any).temporary !== true) {
|
||
updatenative(false)
|
||
} else {
|
||
// Temporary extension has been updated in place
|
||
// Open a new tab where Tridactyl will work for convenience
|
||
tabopen()
|
||
}
|
||
}
|
||
})
|
||
|
||
/** Opens optionsUrl for the selected extension in a popup window.
|
||
*
|
||
* NB: Tridactyl cannot run on this page!
|
||
*/
|
||
//#background
|
||
export async function extoptions(...optionNameArgs: string[]) {
|
||
const optionName = optionNameArgs.join(" ")
|
||
const extensions = await Extensions.listExtensions()
|
||
const selectedExtension = extensions.find(ext => ext.name === optionName)
|
||
return winopen("-popup", selectedExtension.optionsUrl)
|
||
}
|
||
|
||
//#content_helper
|
||
import { Readability } from "@mozilla/readability"
|
||
|
||
/**
|
||
* @hidden
|
||
*/
|
||
//#content_helper
|
||
export async function readerurl() {
|
||
document.querySelectorAll(".TridactylStatusIndicator").forEach(ind => ind.parentNode.removeChild(ind))
|
||
const article = new Readability(document.cloneNode(true) as any as Document).parse()
|
||
article["link"] = window.location.href
|
||
return browser.runtime.getURL("static/reader.html#" + btoa(encodeURIComponent(JSON.stringify(article))))
|
||
}
|
||
|
||
/**
|
||
* Open the current page as an article in reader view for easier reading. Flags `--tab` and `--window` open the article in new tabs and windows respectively.
|
||
*
|
||
* Use `:reader --old` to use Firefox's built-in reader mode, which Tridactyl can't run on.
|
||
*
|
||
* __NB:__ the reader page is a privileged environment which has access to all Tridactyl functions, notably the native messenger if you have it installed. We are parsing untrusted web-content to run in this environment. Mozilla's readability library will strip out most of these, then we use a sanitation library, `js-xss`, to strip out any remaining unsafe tags, but if there was a serious bug in this library, and a targeted attack against Tridactyl, an attacker could get remote code execution. If you're worried about this, use `:reader --old` instead or only use `:reader` on pages you trust.
|
||
*
|
||
* You may use [userContent.css](http://kb.mozillazine.org/index.php?title=UserContent.css&printable=yes) to enhance or override default styling of the new reader view. The `body` of the page has id `tridactyl-reader` and the article content follows in a `main` tag. Therefore to alter default styling, you can do something like this in your `userContent.css`:
|
||
*
|
||
* ```css
|
||
* #tridactyl-reader > main {
|
||
* width: 80vw !important;
|
||
* text-align: left;
|
||
* }
|
||
* ```
|
||
*
|
||
* Follow [issue #4657](https://github.com/tridactyl/tridactyl/issues/4657) if you would like to find out when we have made a more user-friendly solution.
|
||
*/
|
||
//#content
|
||
export async function reader(...args: string[]) {
|
||
switch(args[0]) {
|
||
case "--old":
|
||
readerold()
|
||
break
|
||
case "--tab":
|
||
tabopen(await readerurl())
|
||
break
|
||
case "--window":
|
||
winopen(await readerurl())
|
||
break
|
||
default:
|
||
open(await readerurl())
|
||
break
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Restore the most recently hidden element. Repeated invocations restore the next-most-recently-hidden element.
|
||
*
|
||
* (Elements can be hidden with `;K` and `:hint -K`.)
|
||
*/
|
||
//#content
|
||
export async function elementunhide() {
|
||
const elem = KILL_STACK.pop()
|
||
elem.className = elem.className.replace("TridactylKilledElem", "")
|
||
}
|
||
// vim: tabstop=4 shiftwidth=4 expandtab
|