Merge branch 'master' of github.com:cmcaine/tridactyl into glacambre-fix_pcgamer_hints

This commit is contained in:
Oliver Blanthorn 2017-11-30 08:58:45 +00:00
commit d9fc24dc62
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
16 changed files with 650 additions and 254 deletions

View file

@ -1,4 +1,6 @@
#!/usr/bin/env bash
shopt -s globstar
sed -i '/<\/body>/s@^@<script src="/content.js"></script><link rel="stylesheet" href="/static/content.css"><link rel="stylesheet" href="/static/hint.css">@' $1
sed -i.bak '/<\/body>/s@^@<script src="/content.js"></script><link rel="stylesheet" href="/static/content.css"><link rel="stylesheet" href="/static/hint.css">@' "$1"
rm "$1.bak"
#static/docs/modules/_excmds_.html

View file

@ -93,7 +93,7 @@ def content(lines, context):
sig = Signature(block.split('\n')[0])
return "cmd_params.set('{sig.name}', ".format(**locals()) + dict_to_js(sig.params) + """)
{sig.raw}
messageActiveTab(
return messageActiveTab(
"excmd_content",
"{sig.name}",
""".format(**locals()) + str(list(sig.params.keys())).replace("'","") + """,

View file

@ -1,5 +1,8 @@
#!/usr/bin/env bash
version=$(git describe --tags)
gitversion=$(git describe --tags | cut -d"-" -f2-)
manversion=$(grep '"version":' ./src/manifest.json | cut -d":" -f2 | tr -d \" | tr -d , | cut -d" " -f2)
version=$manversion-$gitversion
sed -i 's/REPLACE_ME_WITH_THE_VERSION_USING_SED/'$version'/' ./build/background.js
sed -i.bak 's/REPLACE_ME_WITH_THE_VERSION_USING_SED/'$version'/' ./build/background.js
rm ./build/background.js.bak

View file

@ -1,5 +1,5 @@
#!/bin/sh
dest=generated/static/docs
typedoc --out $dest src --ignoreCompilerErrors
find $dest -name *.html -exec ./scripts/commandline_injector.sh '{}' \;
find $dest -name \*.html -exec ./scripts/commandline_injector.sh '{}' \;
cp -r $dest build/static/

View file

@ -3,4 +3,9 @@
# Combine newtab markdown and template
cd src/static
sed "/REPLACETHIS/s/REPLACETHIS/marked newtab.md/e" newtab.template.html > ../../generated/static/newtab.html
newtab="../../generated/static/newtab.html"
sed "/REPLACETHIS/,$ d" newtab.template.html > "$newtab"
marked newtab.md >> "$newtab"
sed "1,/REPLACETHIS/ d" newtab.template.html >> "$newtab"

View file

@ -20,6 +20,12 @@ async function add_beta(versionstr) {
})
}
function save_manifest(filename, manifest) {
// Save file
const fs = require('fs')
fs.writeFileSync(filename, JSON.stringify(manifest, null, 4))
}
async function main() {
let filename, manifest
switch (process.argv[2]) {
@ -28,19 +34,18 @@ async function main() {
filename = './src/manifest.json'
manifest = require('.' + filename)
manifest.version = bump_version(manifest.version, Number(process.argv[3]))
save_manifest(filename, manifest)
exec(`git add ${filename} && git commit -m 'release ${manifest.version}' && git tag ${manifest.version}`)
break
case 'beta':
filename = './build/manifest.json'
manifest = require('.' + filename)
manifest.version = await add_beta(manifest.version)
save_manifest(filename, manifest)
break
default:
throw "Unknown command!"
}
// Save file
const fs = require('fs')
fs.writeFile(filename, JSON.stringify(manifest, null, 4), err=>err && console.error(err))
}
main()

View file

@ -13,6 +13,7 @@ import * as Fuse from 'fuse.js'
import {enumerate} from './itertools'
import {toNumber} from './convert'
import * as Messaging from './messaging'
import {browserBg} from './lib/webext'
const DEFAULT_FAVICON = browser.extension.getURL("static/defaultFavicon.svg")
@ -55,9 +56,7 @@ export abstract class CompletionSource {
return this._state
}
next(inc = 1): boolean {
return false
}
abstract next(inc?: number): boolean
prev(inc = 1): boolean {
return this.next(-1*inc)
@ -319,8 +318,9 @@ class HistoryCompletionOption extends CompletionOptionHTML implements Completion
constructor(public value: string, page: browser.history.HistoryItem) {
super()
// Two character buffer properties prefix
// Push prefix before padding so we don't match on whitespace
if (! page.title) {
page.title = new URL(page.url).host
}
// Push properties we want to fuzmatch on
this.fuseKeys.push(page.title, page.url) // weight by page.visitCount
@ -346,11 +346,6 @@ function sleep(ms: number) {
export class HistoryCompletionSource extends CompletionSourceFuse {
public options: HistoryCompletionOption[]
// TODO:
// - store the exstr and trigger redraws on user or data input without
// callback faffery
// - sort out the element redrawing.
constructor(private _parent) {
super(
[
@ -361,45 +356,81 @@ export class HistoryCompletionSource extends CompletionSourceFuse {
"HistoryCompletionSource", "History"
)
this.updateOptions()
this._parent.appendChild(this.node)
}
private async updateOptions(exstr?: string) {
/* console.log('updateOptions', this.optionContainer) */
// this sleep stops input from being blocked, but also breaks :open until something is typed
// await sleep(0)
const history: browser.history.HistoryItem[] =
await Messaging.message("commandline_background", "history")
public async filter(exstr: string) {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
const options = []
// Get alternative tab, defined as last accessed tab.
history.sort((a, b) => { return a.lastVisitTime < b.lastVisitTime ? 1 : -1 })
for (const page of history) {
options.push(new HistoryCompletionOption(
page.url,
page,
))
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === 'hidden') {
this.state = 'normal'
}
} else {
this.state = 'hidden'
return
}
/* console.log('updateOptions end', this.waiting, this.optionContainer) */
this.options = options
this.options = (await this.scoreOptions(query, 10)).map(
page => new HistoryCompletionOption(page.url, page)
)
this.updateChain()
}
async onInput(exstr) {
// Schedule an update, if you like. Not very useful for buffers, but
// will be for other things.
this.updateOptions()
updateChain() {
// Options are pre-trimmed to the right length.
this.options.forEach(option => option.state = 'normal')
// Call concrete class
this.updateDisplay()
}
onInput() {}
private frecency(item: browser.history.HistoryItem) {
// Doesn't actually care about recency yet.
return item.visitCount * -1
}
private async scoreOptions(query: string, n: number) {
if (! query) {
return (await browserBg.topSites.get()).slice(0, n)
} else {
// Search history, dedupe and sort by frecency
let history = await browserBg.history.search({
text: query,
maxResults: 500,
startTime: 0
})
// Remove entries with duplicate URLs
const dedupe = new Map()
for (const page of history) {
if (dedupe.has(page.url)) {
if (dedupe.get(page.url).title.length < page.title.length) {
dedupe.set(page.url, page)
}
} else {
dedupe.set(page.url, page)
}
}
history = [...dedupe.values()]
history.sort((a, b) => this.frecency(a) - this.frecency(b))
return history.slice(0, n)
}
}
}
class BufferCompletionOption extends CompletionOptionHTML implements CompletionOptionFuse {
public fuseKeys = []
constructor(public value: string, tab: browser.tabs.Tab, isAlternative = false) {
constructor(public value: string, tab: browser.tabs.Tab, public isAlternative = false) {
super()
// Two character buffer properties prefix
let pre = ""
@ -477,7 +508,38 @@ export class BufferCompletionSource extends CompletionSourceFuse {
// will be for other things.
this.updateOptions()
}
setStateFromScore(scoredOpts: ScoredOption[]){super.setStateFromScore(scoredOpts, true)}
setStateFromScore(scoredOpts: ScoredOption[]){
super.setStateFromScore(scoredOpts, true)
}
/** Score with fuse unless query is an integer or a single # */
scoredOptions(query: string, options = this.options): ScoredOption[] {
const args = query.split(/\s+/gu)
if (args.length === 1) {
if (Number.isInteger(Number(args[0]))) {
const index = (Number(args[0]) - 1).mod(options.length)
return [{
index,
option: options[index],
score: 0,
}]
} else if (args[0] === '#') {
for (const [index, option] of enumerate(options)) {
if (option.isAlternative) {
return [{
index,
option,
score: 0,
}]
}
}
}
}
// If not yet returned...
return super.scoredOptions(query, options)
}
}
// {{{ UNUSED: MANAGING ASYNC CHANGES

149
src/config.ts Normal file
View file

@ -0,0 +1,149 @@
// Sketch
//
// Need an easy way of getting and setting settings
// If a setting is not set, the default should probably be returned.
// That probably means that binds etc. should be per-key?
//
// We should probably store all settings in memory, and only load from storage on startup and when we set it
//
// Really, we'd like a way of just letting things use the variables
//
const CONFIGNAME = "userconfig"
type StorageMap = browser.storage.StorageMap
// make a naked object
function o(object){
return Object.assign(Object.create(null),object)
}
// TODO: have list of possibilities for settings, e.g. hintmode: reverse | normal
let USERCONFIG = o({})
const DEFAULTS = o({
"nmaps": o({
"o": "fillcmdline open",
"O": "current_url open",
"w": "fillcmdline winopen",
"W": "current_url winopen",
"t": "fillcmdline tabopen",
"]]": "followpage next",
"[[": "followpage prev",
"[c": "urlincrement -1",
"]c": "urlincrement 1",
"T": "current_url tabopen",
"yy": "clipboard yank",
"ys": "clipboard yankshort",
"yc": "clipboard yankcanon",
"p": "clipboard open",
"P": "clipboard tabopen",
"j": "scrollline 10",
"k": "scrollline -10",
"h": "scrollpx -50",
"l": "scrollpx 50",
"G": "scrollto 100",
"gg": "scrollto 0",
"H": "back",
"L": "forward",
"d": "tabclose",
"u": "undo",
"r": "reload",
"R": "reloadhard",
"gi": "focusinput -l",
"gt": "tabnext_gt",
"gT": "tabprev",
"g^": "tabfirst",
"g$": "tablast",
"gr": "reader",
"gu": "urlparent",
"gU": "urlroot",
":": "fillcmdline",
"s": "fillcmdline open search",
"S": "fillcmdline tabopen search",
"M": "gobble 1 quickmark",
"xx": "something",
// "B": "fillcmdline bufferall",
"b": "fillcmdline buffer",
"ZZ": "qall",
"f": "hint",
"F": "hint -b",
";i": "hint -i",
";I": "hint -I",
";y": "hint -y",
";p": "hint -p",
";;": "hint -;",
";#": "hint -#",
"I": "mode ignore",
"a": "current_url bmark",
"A": "bmark",
}),
"searchengine": "google",
"storageloc": "sync",
"hintchars": "hjklasdfgyuiopqwertnmzxcvb",
"hintorder": "normal",
})
// currently only supports 2D or 1D storage
export function get(target, property?){
if (property !== undefined){
if (USERCONFIG[target] !== undefined){
return USERCONFIG[target][property] || DEFAULTS[target][property]
}
else return DEFAULTS[target][property]
}
if (typeof DEFAULTS[target] === "object") return Object.assign(o({}),DEFAULTS[target],USERCONFIG[target])
else return USERCONFIG[target] || DEFAULTS[target]
}
// if you don't specify a property and you should, this will wipe everything
export function set(target, value, property?){
if (property !== undefined){
if (USERCONFIG[target] === undefined) USERCONFIG[target] = o({})
USERCONFIG[target][property] = value
} else USERCONFIG[target] = value
// Always save
save(get("storageloc"))
}
export function unset(target, property?){
if (property !== undefined){
delete USERCONFIG[target][property]
} else delete USERCONFIG[target]
save(get("storageloc"))
}
export async function save(storage: "local" | "sync" = "sync"){
// let storageobj = storage == "local" ? browser.storage.local : browser.storage.sync
// storageobj.set({CONFIGNAME: USERCONFIG})
let settingsobj = o({})
settingsobj[CONFIGNAME] = USERCONFIG
if (storage == "local") browser.storage.local.set(settingsobj)
else browser.storage.sync.set(settingsobj)
}
// Read all user configuration on start
// Legacy config gets loaded first
let legacy_nmaps = {}
browser.storage.sync.get("nmaps").then(nmaps => {
legacy_nmaps = nmaps["nmaps"]
browser.storage.sync.get(CONFIGNAME).then(settings => {
schlepp(settings[CONFIGNAME])
// Local storage overrides sync
browser.storage.local.get(CONFIGNAME).then(settings => {
schlepp(settings[CONFIGNAME])
USERCONFIG["nmaps"] = Object.assign(legacy_nmaps, USERCONFIG["nmaps"])
})
})
})
function schlepp(settings){
// "Import" is a reserved word so this will have to do
Object.assign(USERCONFIG,settings)
}
browser.storage.onChanged.addListener(
(changes, areaname) => {
if (CONFIGNAME in changes) {
Object.assign(USERCONFIG, changes[CONFIGNAME].newValue)
}
}
)

View file

@ -224,23 +224,31 @@ export function isVisible (element: Element) {
/** Get all elements that match the given selector
*
* @param selector `the CSS selector to choose elements with
* @param filter filter to use to further chose items, or null for all
* @param filters filter to use (in thre given order) to further chose
* items, or [] for all
*/
export function getElemsBySelector(selector: string, filter: ElementFilter) {
export function getElemsBySelector(selector: string,
filters: Array<ElementFilter>) {
let elems = Array.from(document.querySelectorAll(selector))
return filter ? elems.filter(filter) : elems
for (let filter of filters) {
elems = elems.filter(filter)
}
return elems
}
/** Get the nth input element on a page
*
* @param nth the element index, can be negative to start at the end
* @param filter filter to use to further chose items, or null for all
* @param filters filter to use (in thre given order) to further chose
* items, or [] for all
*/
export function getNthElement(selectors: string, nth: number,
filter: ElementFilter) {
filters: Array<ElementFilter>): HTMLElement {
let inputs = getElemsBySelector(selectors, filter)
let inputs = getElemsBySelector(selectors, filters)
if (inputs.length) {
let index = Number(nth).clamp(-inputs.length, inputs.length - 1)

View file

@ -68,6 +68,8 @@ import * as CommandLineBackground from './commandline_background'
//#content_helper
import * as DOM from './dom'
import * as config from './config'
/** @hidden */
//#background_helper
@ -106,6 +108,7 @@ function hasScheme(uri: string) {
/** @hidden */
function searchURL(provider: string, query: string) {
if (provider == "search") provider = config.get("searchengine")
if (SEARCH_URLS.has(provider)) {
const url = new URL(SEARCH_URLS.get(provider) + encodeURIComponent(query))
// URL constructor doesn't convert +s because they're valid literals in
@ -148,8 +151,8 @@ function forceURI(maybeURI: string): string {
if (e.name !== 'TypeError') throw e
}
// Else search google
return searchURL('google', maybeURI).href
// Else search $searchengine
return searchURL('search', maybeURI).href
}
/** @hidden */
@ -247,6 +250,9 @@ export async function reloadhard(n = 1) {
- 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 google
Related settings:
"searchengine": "google" or any of [[SEARCH_URLS]]
*/
//#content
export function open(...urlarr: string[]) {
@ -360,6 +366,21 @@ export function urlparent (){
}
}
/** Returns the url of links that have a matching rel.
Don't bind to this: it's an internal function.
@hidden
*/
//#content
export function geturlsforlinks(rel: string){
let elems = document.querySelectorAll("link[rel='" + rel + "']") as NodeListOf<HTMLLinkElement>
console.log(rel, elems)
if (elems)
return Array.prototype.map.call(elems, x => x.href)
return []
}
//#background
export function zoom(level=0){
level = level > 3 ? level / 100 : level
@ -369,12 +390,12 @@ export function zoom(level=0){
//#background
export async function reader() {
if (await l(firefoxVersionAtLeast(58))) {
let aTab = await activeTab()
if (aTab.isArticle) {
browser.tabs.toggleReaderMode()
} // else {
// // once a statusbar exists an error can be displayed there
// }
let aTab = await activeTab()
if (aTab.isArticle) {
browser.tabs.toggleReaderMode()
} // else {
// // once a statusbar exists an error can be displayed there
// }
}
}
@ -448,7 +469,7 @@ export function focusinput(nth: number|string) {
fallbackToNumeric = false
let inputs = DOM.getElemsBySelector(INPUTPASSWORD_selectors,
DOM.isSubstantial)
[DOM.isSubstantial])
if (inputs.length) {
inputToFocus = <HTMLElement>inputs[0]
@ -457,7 +478,7 @@ export function focusinput(nth: number|string) {
else if (nth === "-b") {
let inputs = DOM.getElemsBySelector(INPUTTAGS_selectors,
DOM.isSubstantial) as HTMLElement[]
[DOM.isSubstantial]) as HTMLElement[]
inputToFocus = inputs.sort(DOM.compareElementArea).slice(-1)[0]
}
@ -468,7 +489,7 @@ export function focusinput(nth: number|string) {
let index = isNaN(<number>nth) ? 0 : <number>nth
inputToFocus = DOM.getNthElement(INPUTTAGS_selectors,
index, DOM.isSubstantial)
index, [DOM.isSubstantial])
}
if (inputToFocus) inputToFocus.focus()
@ -482,29 +503,64 @@ document.addEventListener("focusin",e=>{if (DOM.isTextEditable(e.target as HTMLE
// {{{ TABS
/** Switch to the next tab by index (position on tab bar), wrapping round.
/** Switch to the tab by index (position on tab bar), wrapping round.
optional increment is number of tabs forwards to move.
@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) {
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) {
// Get an array of tabs in the current window
let current_window = await browser.windows.getCurrent()
let tabs = await browser.tabs.query({windowId: current_window.id})
// Derive the index we want
let desiredIndex = ((await activeTab()).index + increment).mod(tabs.length)
// Find and switch to the tab with that index
let desiredTab = tabs.find((tab: any) => {
return tab.index === desiredIndex
})
tabSetActive(desiredTab.id)
tabIndexSetActive((await activeTab()).index + increment + 1)
}
/** 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 function tabprev(increment = 1) {
tabnext(increment * -1)
export async function tabnext_gt(index?: number) {
if (index === undefined) {
tabnext()
} else {
tabIndexSetActive(index)
}
}
/** Switch to the previous tab, wrapping round.
If increment is specified, move that many tabs backwards.
*/
//#background
export async function tabprev(increment = 1) {
tabIndexSetActive((await activeTab()).index - increment + 1)
}
/** Switch to the first tab. */
//#background
export async function tabfirst() {
tabIndexSetActive(1)
}
/** Switch to the last tab. */
//#background
export async function tablast() {
tabIndexSetActive(0)
}
/** Like [[open]], but in a new tab */
@ -516,24 +572,81 @@ export async function tabopen(...addressarr: string[]) {
browser.tabs.create({url: uri})
}
//#background
export async function tabduplicate(id?: number){
id = id ? id : (await activeTabId())
browser.tabs.duplicate(id)
}
/** Resolve a tab index to the tab id of the corresponding tab in this window.
//#background
export async function tabdetach(id?: number){
id = id ? id : (await activeTabId())
browser.windows.create({tabId: id})
}
@param index
1-based index of the tab to target. Wraps such that 0 = last tab, -1 =
penultimate tab, etc.
//#background
export async function tabclose(ids?: number[] | number) {
if (ids !== undefined) {
browser.tabs.remove(ids)
if undefined, return activeTabId()
@hidden
*/
//#background_helper
async function idFromIndex(index?: number): Promise<number> {
if (index !== undefined) {
// Wrap
index = (index - 1).mod(
(await l(browser.tabs.query({currentWindow: true}))).length)
+ 1
// Return id of tab with that index.
return (await l(browser.tabs.query({
currentWindow: true,
index: index - 1,
})))[0].id
} else {
// Close the current tab
return await activeTabId()
}
}
/** 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)
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) {
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) {
browser.windows.create({tabId: await idFromIndex(index)})
}
/** 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[]) {
if (indexes.length > 0) {
const idsPromise = indexes.map(index => idFromIndex(Number(index)))
browser.tabs.remove(await Promise.all(idsPromise))
} else {
// Close current tab
browser.tabs.remove(await activeTabId())
}
}
@ -558,17 +671,29 @@ export async function undo(){
}
}
/** Move the current tab to be just in front of the index specified.
Known bug: This supports relative movement, but autocomple 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 is the first index. 0 is the last index. -1 is the penultimate, etc.
*/
//#background
export async function tabmove(n?: string) {
let aTab = await activeTab(),
m: number
if (!n) {
browser.tabs.move(aTab.id, {index: -1})
return
} else if (n.startsWith("+") || n.startsWith("-")) {
m = Math.max(0, Number(n) + aTab.index)
} else m = Number(n)
browser.tabs.move(aTab.id, {index: m})
export async function tabmove(index = "0") {
const aTab = await activeTab()
let newindex: number
if (index.startsWith("+") || index.startsWith("-")) {
newindex = Math.max(0, Number(index) + aTab.index)
} else newindex = Number(index) - 1
browser.tabs.move(aTab.id, {index: newindex})
}
/** Pin the current tab */
@ -735,14 +860,31 @@ export async function current_url(...strarr: string[]){
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/.
Unfortunately, javascript can only give us the `clipboard` clipboard, not e.g. the X selection clipboard.
*/
//#background
export async function clipboard(excmd: "open"|"yank"|"tabopen" = "open", ...toYank: string[]) {
export async function clipboard(excmd: "open"|"yank"|"yankshort"|"yankcanon"|"tabopen" = "open", ...toYank: string[]) {
let content = toYank.join(" ")
let url = ""
let urls = []
switch (excmd) {
case 'yankshort':
urls = await geturlsforlinks("shortlink")
if (urls.length > 0) {
messageActiveTab("commandline_frame", "setClipboard", [urls[0]])
break
}
case 'yankcanon':
urls = await geturlsforlinks("canonical")
if (urls.length > 0) {
messageActiveTab("commandline_frame", "setClipboard", [urls[0]])
break
}
case 'yank':
await messageActiveTab("commandline_content", "focus")
content = (content == "") ? (await activeTab()).url : content
@ -766,43 +908,46 @@ export async function clipboard(excmd: "open"|"yank"|"tabopen" = "open", ...toYa
}
// {{{ Buffer/completion stuff
// TODO: Move autocompletions out of excmds.
/** Ported from Vimperator. */
/** Equivalent to `fillcmdline buffer`
Sort of Vimperator alias
*/
//#background
export async function tabs() {
fillcmdline("buffer")
}
/** Equivalent to `fillcmdline buffer`
Sort of Vimperator alias
*/
//#background
export async function buffers() {
tabs()
}
/** Change active tab */
/** Change active tab.
@param index
Starts at 1. 0 refers to last tab, -1 to penultimate tab, etc.
"#" means the tab that was last accessed in this window
*/
//#background
export async function buffer(n?: number | string) {
if (!n || Number(n) == 0) return // Vimperator index starts at 1
if (n === "#") {
n =
export async function buffer(index: number | '#') {
if (index === "#") {
// Switch to the most recently accessed buffer
tabIndexSetActive(
(await browser.tabs.query({currentWindow: true})).sort((a, b) => {
return a.lastAccessed < b.lastAccessed ? 1 : -1
})[1].index + 1
}
if (Number.isInteger(Number(n))) {
tabSetActive(
(await browser.tabs.query({
currentWindow: true,
index: Number(n) - 1,
}))[0].id
)
} else if (Number.isInteger(Number(index))) {
tabIndexSetActive(Number(index))
}
}
/*/1** Set tab with index of n belonging to window with id of m to active *1/ */
/*//#background */
/*export async function bufferall(m?: number, n?: number) { */
/* // TODO */
/*} */
// }}}
// }}}
@ -830,12 +975,9 @@ export async function buffer(n?: number | string) {
- [[reset]]
*/
//#background
export async function bind(key: string, ...bindarr: string[]){
export function bind(key: string, ...bindarr: string[]){
let exstring = bindarr.join(" ")
let nmaps = (await browser.storage.sync.get("nmaps"))["nmaps"]
nmaps = (nmaps == undefined) ? Object.create(null) : nmaps
nmaps[key] = exstring
browser.storage.sync.set({nmaps})
config.set("nmaps",exstring,key)
}
/** Unbind a sequence of keys so that they do nothing at all.
@ -859,6 +1001,9 @@ export async function unbind(key: string){
*/
//#background
export async function reset(key: string){
config.unset("nmaps",key)
// Code for dealing with legacy binds
let nmaps = (await browser.storage.sync.get("nmaps"))["nmaps"]
nmaps = (nmaps == undefined) ? {} : nmaps
delete nmaps[key]
@ -885,6 +1030,46 @@ export async function quickmark(key: string) {
await bind("gw" + key, "winopen", address)
}
//#background
export function get(target: string, property?: string){
console.log(config.get(target,property))
}
/** Set a setting to a value
Currently, this only supports string settings without any whitespace
(i.e. not nmaps.)
It can be used on any string <-> string settings found [here](/static/docs/modules/_config_.html#defaults)
*/
//#background
export function set(setting: string, value?: string){
// We don't support setting objects yet
if (setting != "nmaps"){
if (value !== undefined){
config.set(setting,value)
} else fillcmdline_notrail("set " + setting + " " + config.get(setting))
}
}
//#background
export function unset(target: string, property?: string){
config.unset(target,property)
}
// not required as we automatically save all config
////#background
//export function saveconfig(){
// config.save(config.get("storageloc"))
//}
////#background
//export function mktridactylrc(){
// saveconfig()
//}
// }}}
// {{{ HINTMODE
@ -893,18 +1078,32 @@ export async function quickmark(key: string) {
import * as hinting from './hinting_background'
/** Hint a page.
*
* Pass -b as first argument to open hinted page in background.
* -y copies the link's target to the clipboard.
* -p copies an element's text to the clipboard.*/
@param option
- -b open in background
- -y copy (yank) link's target to clipboard
- -p copy an element's text to the clipboard
- -i view an image
- -I view an image in a new tab
- -; focus an element
- -# yank an element's anchor URL to clipboard
- -c [selector] hint links that match the css selector
- `bind ;c hint -c [class*="expand"],[class="togg"]` works particularly well on reddit and HN
Related settings:
"hintchars": "hjklasdfgyuiopqwertnmzxcvb"
"hintorder": "normal" or "reverse"
*/
//#background
export function hint(option?: "-b") {
export function hint(option?: string, selectors="") {
if (option === '-b') hinting.hintPageOpenInBackground()
else if (option === "-y") hinting.hintPageYank()
else if (option === "-p") hinting.hintPageTextYank()
else if (option === "-i") hinting.hintImage(false)
else if (option === "-I") hinting.hintImage(true)
else if (option === "-;") hinting.hintFocus()
else if (option === "-#") hinting.hintPageAnchorYank()
else if (option === "-c") hinting.hintPageSimple(selectors)
else hinting.hintPageSimple()
}

View file

@ -10,17 +10,18 @@
Redraw on reflow
*/
import {elementsByXPath, isVisible, mouseEvent} from './dom'
import * as DOM from './dom'
import {log} from './math'
import {permutationsWithReplacement, islice, izip, map} from './itertools'
import {hasModifiers} from './keyseq'
import state from './state'
import {messageActiveTab} from './messaging'
import * as config from './config'
/** Simple container for the state of a single frame's hints. */
class HintState {
public focusedHint: Hint
readonly hintHost = document.createElement('div')
readonly hintHost = html`<div class="TridactylHintHost">`
readonly hints: Hint[] = []
public filter = ''
public hintchars = ''
@ -63,16 +64,19 @@ export function hintPage(
}
/** vimperator-style minimal hint names */
function* hintnames(hintchars = HINTCHARS): IterableIterator<string> {
function* hintnames(hintchars = config.get("hintchars")): IterableIterator<string> {
let taglen = 1
while (true) {
yield* map(permutationsWithReplacement(hintchars, taglen), e=>e.join(''))
yield* map(permutationsWithReplacement(hintchars, taglen), e=>{
if (config.get("hintorder") == "reverse") e = e.reverse()
return e.join('')
})
taglen++
}
}
/** Uniform length hintnames */
function* hintnames_uniform(n: number, hintchars = HINTCHARS): IterableIterator<string> {
function* hintnames_uniform(n: number, hintchars = config.get("hintchars")): IterableIterator<string> {
if (n <= hintchars.length)
yield* islice(hintchars[Symbol.iterator](), n)
else {
@ -80,7 +84,10 @@ function* hintnames_uniform(n: number, hintchars = HINTCHARS): IterableIterator<
const taglen = Math.ceil(log(n, hintchars.length))
// And return first n permutations
yield* map(islice(permutationsWithReplacement(hintchars, taglen), n),
perm => perm.join(''))
perm => {
if (config.get("hintorder") == "reverse") perm = perm.reverse()
return perm.join('')
})
}
}
@ -136,9 +143,6 @@ class Hint {
}
}
const HINTCHARS = 'hjklasdfgyuiopqwertnmzxcvb'
/* const HINTCHARS = 'asdf' */
/** Show only hints prefixed by fstr. Focus first match */
function filter(fstr) {
const active: Hint[] = []
@ -191,24 +195,30 @@ function pushKey(ke) {
1. Within viewport
2. Not hidden by another element
*/
function hintables() {
return Array.from(document.querySelectorAll(HINTTAGS_selectors)).filter(isVisible)
function hintables(selectors=HINTTAGS_selectors) {
return DOM.getElemsBySelector(selectors, [DOM.isVisible])
}
function elementswithtext() {
return Array.from(document.querySelectorAll(HINTTAGS_text_selectors)).filter(
isVisible
).filter(hint => {
return hint.textContent != ""
})
return DOM.getElemsBySelector("*",
[DOM.isVisible, hint => {
return hint.textContent != ""
}]
)
}
/** Get array of images in the viewport
*/
function hintableImages() {
/* return [...elementsByXPath(HINTTAGS)].filter(isVisible) as any as Element[] */
return Array.from(document.querySelectorAll(HINTTAGS_img_selectors)).filter(
isVisible)
return DOM.getElemsBySelector(HINTTAGS_img_selectors, [DOM.isVisible])
}
/** Get arrat of "anchors": elements which have id or name and can be addressed
* with the hash/fragment in the URL
*/
function anchors() {
return DOM.getElemsBySelector(HINTTAGS_anchor_selectors, [DOM.isVisible])
}
// CSS selectors. More readable for web developers. Not dead. Leaves browser to care about XML.
@ -246,25 +256,16 @@ select,
[tabindex]
`
const HINTTAGS_text_selectors = `
input:not([type=hidden]):not([disabled]),
a,
area,
iframe,
textarea,
button,
p,
div,
pre,
code,
span
`
const HINTTAGS_img_selectors = `
img,
[src]
`
const HINTTAGS_anchor_selectors = `
[id],
[name]
`
import {activeTab, browserBg, l, firefoxVersionAtLeast} from './lib/webext'
async function openInBackground(url: string) {
@ -290,7 +291,7 @@ function simulateClick(target: HTMLElement) {
) {
browserBg.tabs.create({url: (target as HTMLAnchorElement).href})
} else {
mouseEvent(target, "click")
DOM.mouseEvent(target, "click")
// Sometimes clicking the element doesn't focus it sufficiently.
target.focus()
}
@ -309,8 +310,8 @@ function hintPageOpenInBackground() {
})
}
function hintPageSimple() {
hintPage(hintables(), hint=>{
function hintPageSimple(selectors=HINTTAGS_selectors) {
hintPage(hintables(selectors), hint=>{
simulateClick(hint.target)
})
}
@ -327,6 +328,20 @@ function hintPageYank() {
})
}
/** Hint anchors and yank the URL on selection
*/
function hintPageAnchorYank() {
hintPage(anchors(), hint=>{
let anchorUrl = new URL(window.location.href)
anchorUrl.hash = hint.target.id || hint.target.name;
messageActiveTab("commandline_frame", "setClipboard", [anchorUrl.href])
})
}
/** Hint images, opening in the same tab, or in a background tab
*
* @param inBackground opens the image source URL in a background tab,
@ -366,6 +381,7 @@ addListener('hinting_content', attributeCaller({
hintPageSimple,
hintPageYank,
hintPageTextYank,
hintPageAnchorYank,
hintPageOpenInBackground,
hintImage,
hintFocus,

View file

@ -19,8 +19,13 @@ export async function hintPageYank() {
export async function hintPageTextYank() {
return await messageActiveTab('hinting_content', 'hintPageTextYank')
}
export async function hintPageSimple() {
return await messageActiveTab('hinting_content', 'hintPageSimple')
export async function hintPageAnchorYank() {
return await messageActiveTab('hinting_content', 'hintPageAnchorYank')
}
export async function hintPageSimple(selectors?) {
return await messageActiveTab('hinting_content', 'hintPageSimple',[selectors])
}
export async function hintPageOpenInBackground() {

View file

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

View file

@ -3,83 +3,10 @@
differs from Vim in that no map may be a prefix of another map (e.g. 'g' and 'gg' cannot both be maps). This simplifies the parser.
*/
// Normal-mode mappings.
// keystr -> ex_str
// TODO: Move these into a tridactyl-wide state namespace
// TODO: stop stealing keys from "insert mode"
// r -> refresh page is particularly unhelpful
// Can't stringify a map -> just use an object
export const DEFAULTNMAPS = {
"o": "fillcmdline open",
"O": "current_url open",
"w": "fillcmdline winopen",
"W": "current_url winopen",
"t": "fillcmdline tabopen",
//["t": "fillcmdline tabopen", // for now, use mozilla completion
"]]": "followpage next",
"[[": "followpage prev",
"[c": "urlincrement -1",
"]c": "urlincrement 1",
"T": "current_url tabopen",
"yy": "clipboard yank",
"p": "clipboard open",
"P": "clipboard tabopen",
"j": "scrollline 10",
"k": "scrollline -10",
"h": "scrollpx -50",
"l": "scrollpx 50",
"G": "scrollto 100",
"gg": "scrollto 0",
"H": "back",
"L": "forward",
"d": "tabclose",
"u": "undo",
"r": "reload",
"R": "reloadhard",
"gi": "focusinput -l",
"gt": "tabnext",
"gT": "tabprev",
"gr": "reader",
"gu": "urlparent",
"gU": "urlroot",
":": "fillcmdline",
"s": "fillcmdline open google",
"S": "fillcmdline tabopen google",
"M": "gobble 1 quickmark",
"xx": "something",
// "B": "fillcmdline bufferall",
"b": "fillcmdline buffer",
"ZZ": "qall",
"f": "hint",
"F": "hint -b",
";i": "hint -i",
";I": "hint -I",
";y": "hint -y",
";p": "hint -p",
";;": "hint -;",
"I": "mode ignore",
"a": "current_url bmark",
"A": "bmark",
// Special keys must be prepended with 🄰
// ["🄰Backspace", "something"],
}
import * as config from '../config'
let nmaps = Object.assign(Object.create(null), DEFAULTNMAPS)
let nmaps = config.get("nmaps")
// Allow config to be changed in settings
// TODO: make this more general
browser.storage.sync.get("nmaps").then(lazyloadconfig)
async function lazyloadconfig(storageResult){
nmaps = Object.assign(Object.create(null), DEFAULTNMAPS, storageResult.nmaps)
console.log(nmaps)
}
browser.storage.onChanged.addListener(
(changes, areaname) => {
if (areaname == "sync") {
browser.storage.sync.get("nmaps").then(lazyloadconfig)
}
})
// Split a string into a number prefix and some following keys.
function keys_split_count(keys: string[]){
@ -96,7 +23,7 @@ function keys_split_count(keys: string[]){
// Given a valid keymap, resolve it to an ex_str
function resolve_map(map) {
// TODO: This needs to become recursive to allow maps to be defined in terms of other maps.
return nmaps[map]
return config.get("nmaps")[map]
}
// Valid keystr to ex_str by splitting count, resolving keystr and appending count as final argument.
@ -116,7 +43,7 @@ function possible_maps(keys): string[] {
let [count, keystr] = keys_split_count(keys)
// Short circuit or search maps.
if (Object.getOwnPropertyNames(nmaps).includes(keystr)) {
if (Object.getOwnPropertyNames(config.get("nmaps")).includes(keystr)) {
return [keystr,]
} else {
// Efficiency: this can be short-circuited.
@ -126,7 +53,7 @@ function possible_maps(keys): string[] {
// A list of maps that start with the fragment.
export function completions(fragment): string[] {
let posskeystrs = Array.from(Object.keys(nmaps))
let posskeystrs = Array.from(Object.keys(config.get("nmaps")))
return posskeystrs.filter((key)=>key.startsWith(fragment))
}

View file

@ -66,6 +66,10 @@ input {
overflow: hidden;
}
#completions table tr td {
overflow: hidden;
}
#completions img {
display: inline;
vertical-align: middle;

View file

@ -21,5 +21,16 @@ span.TridactylHint {
transition: unset;
}
.TridactylHintElem { background-color: yellow; }
.TridactylHintActive { background-color: #88FF00; }
.TridactylHintElem {
background-color: rgba(255, 255, 0, 0.25);
outline: 1px solid #8F5902;
}
.TridactylHintActive {
background-color: #88FF00;
outline: 1px solid #CC0000;
}
div.TridactylHintHost {
position: static !important;
}