This commit is contained in:
Babil Golam Sarwar 2018-06-25 07:42:31 +10:00
commit 54aca0fcf7
No known key found for this signature in database
GPG key ID: 8EA67D99F433E92D
34 changed files with 1293 additions and 467 deletions

View file

@ -1,5 +1,28 @@
# Tridactyl changelog
## Release 1.13.1 / 2018-06-20
* New features
* `bufferall` bound to `B` by default shows you tabs in all windows.
* Container management with `container{create,close,update,delete}`, `viewcontainers` and `tabopen -c [container name] URL`
* see `help containercreate` for more information
* Mode indicator's border now uses the current container colour
* `set hintnames numeric` for sequential numeric hints. Best used with `set hintfiltermode vimperator-reflow`.
* Changelog now tells you when there's a new changelog that you haven't read.
* `guiset navbar none` removes the navbar totally. Not for the faint-of-heart: you could potentially get trapped if Tridactyl stops working.
* Bug fixes
* `nativeopen` now puts tabs in the same place that `tabopen` would
* `santise tridactyllocal tridactylsync` now works in RC files
* Missing ;w hint winopen bind added
* Fixed minor error with themes not being properly applied on some sites
* Fixed reload bug on :help when there's no hash
* `<C-i>` editor will now always update the field you started in, not just the one you currently have focused.
* "email" input elements can now be focused without errors.
* `urlincrement` no longer throws errors if a link cannot be found.
## Release 1.13.0 / 2018-06-08
* **Potentially breaking changes**

View file

@ -13,7 +13,7 @@ $global:MessengerBinPyName = "native_main.py"
$global:MessengerBinExeName = "native_main.exe"
$global:MessengerBinWrapperFilename = "native_main.bat"
$global:MessengerManifestFilename = "tridactyl.json"
$global:PythonVersionStr = "Python 3"
$global:PythonVersionStr = "^3\."
$global:WinPython3Command = "py -3 -u"
$global:MessengerManifestReplaceStr = "REPLACE_ME_WITH_SED"
@ -46,12 +46,12 @@ $global:DebugDirBase = $DebugDirBase.Trim()
function Get-PythonVersionStatus() {
try {
$pythonVersion = Invoke-Expression `
"$global:WinPython3Command --version"
"$global:WinPython3Command -c ""import sys; print(sys.version)"""
} catch {
$pythonVersion = ""
}
if ($pythonVersion.IndexOf($global:PythonVersionStr) -ge 0) {
if ($pythonVersion -match $global:PythonVersionStr) {
return $true
} else {
return $false

View file

@ -208,6 +208,10 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
There are two ways to do that, the first one is `set allowautofocus false` (if you do this you'll probably also want to set `browser.autofocus` to false in `about:config`). This will prevent the page's `focus()` function from working and could break javascript text editors such as Ace or CodeMirror. Another solution is to use `autocmd TabEnter .* unfocus` in the beta, JS text editors should still work but pages won't steal focus when entering their tabs anymore.
* Help! A website I use is totally blank when I try to use it with Tridactyl enabled!
Try `set noiframeon [space separated list of URLs to match]`. If that doesn't work, please file an issue.
## Contributing
### Building and installing

View file

@ -14,10 +14,11 @@ sed "1,/REPLACETHIS/ d" newtab.template.html >> "$newtabtemp"
# Why think when you can pattern match?
sed "/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/,$ d" "$newtabtemp" > "$newtab"
# Note: If you're going to change this HTML, make sure you don't break the JS in src/newtab.ts
echo """
<input type="checkbox" id="spoilerbutton" />
<label for="spoilerbutton" onclick="">Changelog</label>
<div class="spoiler">
<label for="spoilerbutton" onclick=""><div id="nagbar-changelog">New features!</div>Changelog</label>
<div id="changelog" class="spoiler">
""" >> "$newtab"
$(npm bin)/marked ../../CHANGELOG.md >> "$newtab"
echo """

View file

@ -59,6 +59,7 @@ export async function hide() {
Messaging.addListener(
"commandline_background",
Messaging.attributeCaller({
allWindowTabs,
currentWindowTabs,
history,
recvExStr,

View file

@ -3,6 +3,10 @@
import "./lib/html-tagged-template"
import * as Completions from "./completions"
import { BufferAllCompletionSource } from "./completions/BufferAll"
import { BufferCompletionSource } from "./completions/Buffer"
import { BmarkCompletionSource } from "./completions/Bmark"
import { HistoryCompletionSource } from "./completions/History"
import * as Messaging from "./messaging"
import * as Config from "./config"
import * as SELF from "./commandline_frame"
@ -50,9 +54,10 @@ function getCompletion() {
function enableCompletions() {
if (!activeCompletions) {
activeCompletions = [
new Completions.BufferCompletionSource(completionsDiv),
new Completions.HistoryCompletionSource(completionsDiv),
new Completions.BmarkCompletionSource(completionsDiv),
new BufferAllCompletionSource(completionsDiv),
new BufferCompletionSource(completionsDiv),
new HistoryCompletionSource(completionsDiv),
new BmarkCompletionSource(completionsDiv),
]
const fragment = document.createDocumentFragment()

View file

@ -6,22 +6,23 @@ On each input event, call updateCompletions on the array. That will mutate the a
How to handle cached e.g. buffer information going out of date?
Concrete completion classes have been moved to src/completions/.
*/
import * as Fuse from "fuse.js"
import { enumerate } from "./itertools"
import { toNumber } from "./convert"
import * as Messaging from "./messaging"
import * as config from "./config"
import { browserBg } from "./lib/webext"
const DEFAULT_FAVICON = browser.extension.getURL("static/defaultFavicon.svg")
export const DEFAULT_FAVICON = browser.extension.getURL(
"static/defaultFavicon.svg",
)
// {{{ INTERFACES
type OptionState = "focused" | "hidden" | "normal"
abstract class CompletionOption {
export abstract class CompletionOption {
/** What to fill into cmdline */
value: string
/** Control presentation of the option */
@ -65,7 +66,7 @@ export abstract class CompletionSource {
// Default classes
abstract class CompletionOptionHTML extends CompletionOption {
export abstract class CompletionOptionHTML extends CompletionOption {
public html: HTMLElement
public value
@ -97,18 +98,18 @@ abstract class CompletionOptionHTML extends CompletionOption {
}
}
interface CompletionOptionFuse extends CompletionOptionHTML {
export interface CompletionOptionFuse extends CompletionOptionHTML {
// For fuzzy matching
fuseKeys: any[]
}
type ScoredOption = {
export type ScoredOption = {
index: number
option: CompletionOptionFuse
score: number
}
abstract class CompletionSourceFuse extends CompletionSource {
export abstract class CompletionSourceFuse extends CompletionSource {
public node
public options: CompletionOptionFuse[]
protected lastExstr: string
@ -132,8 +133,8 @@ abstract class CompletionSourceFuse extends CompletionSource {
public async filter(exstr: string) {
this.lastExstr = exstr
this.onInput(exstr)
this.updateChain()
await this.onInput(exstr)
await this.updateChain()
}
updateChain(exstr = this.lastExstr, options = this.options) {
@ -306,366 +307,6 @@ abstract class CompletionSourceFuse extends CompletionSource {
// }}}
// {{{ IMPLEMENTATIONS
class HistoryCompletionOption extends CompletionOptionHTML
implements CompletionOptionFuse {
public fuseKeys = []
constructor(public value: string, page: browser.history.HistoryItem) {
super()
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
// Create HTMLElement
// need to download favicon
const favIconUrl = DEFAULT_FAVICON
// const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
this.html = html`<tr class="HistoryCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td></td>
<td>${page.title}</td>
<td><a class="url" target="_blank" href=${page.url}>${
page.url
}</a></td>
</tr>`
}
}
class BmarkCompletionOption extends CompletionOptionHTML
implements CompletionOptionFuse {
public fuseKeys = []
constructor(
public value: string,
bmark: browser.bookmarks.BookmarkTreeNode,
) {
super()
if (!bmark.title) {
bmark.title = new URL(bmark.url).host
}
// Push properties we want to fuzmatch on
this.fuseKeys.push(bmark.title, bmark.url)
// Create HTMLElement
// need to download favicon
const favIconUrl = DEFAULT_FAVICON
// const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
this.html = html`<tr class="HistoryCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td></td>
<td>${bmark.title}</td>
<td><a class="url" target="_blank" href=${bmark.url}>${
bmark.url
}</a></td>
</tr>`
}
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export class BmarkCompletionSource extends CompletionSourceFuse {
public options: BmarkCompletionOption[]
constructor(private _parent) {
super(["bmarks "], "BmarkCompletionSource", "Bookmarks")
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
this.options = (await this.scoreOptions(query, 10)).map(
page => new BmarkCompletionOption(page.url, page),
)
this.updateChain()
}
updateChain() {
// Options are pre-trimmed to the right length.
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
this.updateDisplay()
}
onInput() {}
private async scoreOptions(query: string, n: number) {
// Search bookmarks, dedupe and sort by frecency
let bookmarks = await browserBg.bookmarks.search({ query })
bookmarks = bookmarks.filter(b => {
try {
return new URL(b.url)
} catch (e) {
return false
}
})
bookmarks.sort((a, b) => b.dateAdded - a.dateAdded)
return bookmarks.slice(0, n)
}
select(option: CompletionOption) {
if (this.lastExstr !== undefined && option !== undefined) {
this.completion = "open " + option.value
option.state = "focused"
this.lastFocused = option
} else {
throw new Error("lastExstr and option must be defined!")
}
}
}
export class HistoryCompletionSource extends CompletionSourceFuse {
public options: HistoryCompletionOption[]
constructor(private _parent) {
super(
["open ", "tabopen ", "winopen "],
"HistoryCompletionSource",
"History",
)
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
this.options = (await this.scoreOptions(query, 10)).map(
page => new HistoryCompletionOption(page.url, page),
)
this.updateChain()
}
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) {
const newtab = browser.runtime.getManifest()["chrome_url_overrides"]
.newtab
const newtaburl = browser.extension.getURL(newtab)
if (!query) {
return (await browserBg.topSites.get())
.filter(page => page.url !== newtaburl)
.slice(0, n)
} else {
// Search history, dedupe and sort by frecency
let history = await browserBg.history.search({
text: query,
maxResults: Number(config.get("historyresults")),
startTime: 0,
})
// Remove entries with duplicate URLs
const dedupe = new Map()
for (const page of history) {
if (page.url !== newtaburl) {
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,
public isAlternative = false,
) {
super()
// Two character buffer properties prefix
let pre = ""
if (tab.active) pre += "%"
else if (isAlternative) {
pre += "#"
this.value = "#"
}
if (tab.pinned) pre += "@"
// Push prefix before padding so we don't match on whitespace
this.fuseKeys.push(pre)
// Push properties we want to fuzmatch on
this.fuseKeys.push(String(tab.index + 1), tab.title, tab.url)
// Create HTMLElement
const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
this.html = html`<tr class="BufferCompletionOption option">
<td class="prefix">${pre.padEnd(2)}</td>
<td><img src=${favIconUrl} /></td>
<td>${tab.index + 1}: ${tab.title}</td>
<td><a class="url" target="_blank" href=${tab.url}>${
tab.url
}</a></td>
</tr>`
}
}
export class BufferCompletionSource extends CompletionSourceFuse {
public options: BufferCompletionOption[]
// TODO:
// - store the exstr and trigger redraws on user or data input without
// callback faffery
// - sort out the element redrawing.
constructor(private _parent) {
super(
["buffer ", "tabclose ", "tabdetach ", "tabduplicate ", "tabmove "],
"BufferCompletionSource",
"Buffers",
)
this.updateOptions()
this._parent.appendChild(this.node)
}
private async updateOptions(exstr?: string) {
/* console.log('updateOptions', this.optionContainer) */
const tabs: browser.tabs.Tab[] = await Messaging.message(
"commandline_background",
"currentWindowTabs",
)
const options = []
// Get alternative tab, defined as last accessed tab.
const alt = tabs.sort((a, b) => {
return a.lastAccessed < b.lastAccessed ? 1 : -1
})[1]
tabs.sort((a, b) => {
return a.index < b.index ? -1 : 1
})
for (const tab of tabs) {
options.push(
new BufferCompletionOption(
(tab.index + 1).toString(),
tab,
tab === alt,
),
)
}
/* console.log('updateOptions end', this.waiting, this.optionContainer) */
this.options = options
this.updateChain()
}
async onInput(exstr) {
// Schedule an update, if you like. Not very useful for buffers, but
// will be for other things.
this.updateOptions()
}
setStateFromScore(scoredOpts: ScoredOption[]) {
super.setStateFromScore(scoredOpts, true)
}
/** Score with fuse unless query is a single # or looks like a buffer index */
scoredOptions(query: string, options = this.options): ScoredOption[] {
const args = query.trim().split(/\s+/gu)
if (args.length === 1) {
// if query is an integer n and |n| < options.length
if (Number.isInteger(Number(args[0]))) {
let index = Number(args[0]) - 1
if (Math.abs(index) < options.length) {
index = index.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
/** If first to modify epoch, commit change. May want to change epoch after commiting. */

101
src/completions/Bmark.ts Normal file
View file

@ -0,0 +1,101 @@
import { browserBg } from "../lib/webext"
import * as Completions from "../completions"
class BmarkCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(
public value: string,
bmark: browser.bookmarks.BookmarkTreeNode,
) {
super()
if (!bmark.title) {
bmark.title = new URL(bmark.url).host
}
// Push properties we want to fuzmatch on
this.fuseKeys.push(bmark.title, bmark.url)
// Create HTMLElement
// need to download favicon
const favIconUrl = Completions.DEFAULT_FAVICON
// const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
this.html = html`<tr class="BmarkCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td class="icon"></td>
<td class="title">${bmark.title}</td>
<td class="content"><a class="url" target="_blank" href=${
bmark.url
}>${bmark.url}</a></td>
</tr>`
}
}
export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
public options: BmarkCompletionOption[]
constructor(private _parent) {
super(["bmarks "], "BmarkCompletionSource", "Bookmarks")
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
this.options = (await this.scoreOptions(query, 10)).map(
page => new BmarkCompletionOption(page.url, page),
)
this.updateChain()
}
updateChain() {
// Options are pre-trimmed to the right length.
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
this.updateDisplay()
}
onInput() {}
private async scoreOptions(query: string, n: number) {
// Search bookmarks, dedupe and sort by frecency
let bookmarks = await browserBg.bookmarks.search({ query })
bookmarks = bookmarks.filter(b => {
try {
return new URL(b.url)
} catch (e) {
return false
}
})
bookmarks.sort((a, b) => b.dateAdded - a.dateAdded)
return bookmarks.slice(0, n)
}
select(option: Completions.CompletionOption) {
if (this.lastExstr !== undefined && option !== undefined) {
this.completion = "open " + option.value
option.state = "focused"
this.lastFocused = option
} else {
throw new Error("lastExstr and option must be defined!")
}
}
}

150
src/completions/Buffer.ts Normal file
View file

@ -0,0 +1,150 @@
import { enumerate } from "../itertools"
import * as Containers from "../lib/containers"
import * as Messaging from "../messaging"
import * as Completions from "../completions"
class BufferCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(
public value: string,
tab: browser.tabs.Tab,
public isAlternative = false,
container: browser.contextualIdentities.ContextualIdentity,
) {
super()
// Two character buffer properties prefix
let pre = ""
if (tab.active) pre += "%"
else if (isAlternative) {
pre += "#"
this.value = "#"
}
if (tab.pinned) pre += "@"
// Push prefix before padding so we don't match on whitespace
this.fuseKeys.push(pre)
// Push properties we want to fuzmatch on
this.fuseKeys.push(String(tab.index + 1), tab.title, tab.url)
// Create HTMLElement
const favIconUrl = tab.favIconUrl
? tab.favIconUrl
: Completions.DEFAULT_FAVICON
this.html = html`<tr class="BufferCompletionOption option container_${
container.color
} container_${container.icon} container_${container.name}">
<td class="prefix">${pre.padEnd(2)}</td>
<td class="container"></td>
<td class="icon"><img src="${favIconUrl}"/></td>
<td class="title">${tab.index + 1}: ${tab.title}</td>
<td class="content"><a class="url" target="_blank" href=${
tab.url
}>${tab.url}</a></td>
</tr>`
}
}
export class BufferCompletionSource extends Completions.CompletionSourceFuse {
public options: BufferCompletionOption[]
// TODO:
// - store the exstr and trigger redraws on user or data input without
// callback faffery
// - sort out the element redrawing.
constructor(private _parent) {
super(
["buffer ", "tabclose ", "tabdetach ", "tabduplicate ", "tabmove "],
"BufferCompletionSource",
"Buffers",
)
this.updateOptions()
this._parent.appendChild(this.node)
}
private async updateOptions(exstr?: string) {
/* console.log('updateOptions', this.optionContainer) */
const tabs: browser.tabs.Tab[] = await Messaging.message(
"commandline_background",
"currentWindowTabs",
)
const options = []
// Get alternative tab, defined as last accessed tab.
const alt = tabs.sort((a, b) => {
return a.lastAccessed < b.lastAccessed ? 1 : -1
})[1]
tabs.sort((a, b) => {
return a.index < b.index ? -1 : 1
})
for (const tab of tabs) {
options.push(
new BufferCompletionOption(
(tab.index + 1).toString(),
tab,
tab === alt,
await Containers.getFromId(tab.cookieStoreId),
),
)
}
/* console.log('updateOptions end', this.waiting, this.optionContainer) */
this.options = options
this.updateChain()
}
async onInput(exstr) {
// Schedule an update, if you like. Not very useful for buffers, but
// will be for other things.
this.updateOptions()
}
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
super.setStateFromScore(scoredOpts, true)
}
/** Score with fuse unless query is a single # or looks like a buffer index */
scoredOptions(
query: string,
options = this.options,
): Completions.ScoredOption[] {
const args = query.trim().split(/\s+/gu)
if (args.length === 1) {
// if query is an integer n and |n| < options.length
if (Number.isInteger(Number(args[0]))) {
let index = Number(args[0]) - 1
if (Math.abs(index) < options.length) {
index = index.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)
}
}

View file

@ -0,0 +1,89 @@
import * as Containers from "../lib/containers"
import * as Messaging from "../messaging"
import * as Completions from "../completions"
class BufferAllCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(
public value: string,
tab: browser.tabs.Tab,
winindex: number,
container: browser.contextualIdentities.ContextualIdentity,
) {
super()
this.value = `${winindex}.${tab.index + 1}`
this.fuseKeys.push(this.value, tab.title, tab.url)
// Create HTMLElement
const favIconUrl = tab.favIconUrl
? tab.favIconUrl
: Completions.DEFAULT_FAVICON
this.html = html`<tr class="BufferAllCompletionOption option container_${
container.color
} container_${container.icon} container_${container.name}">
<td class="prefix"></td>
<td class="container"></td>
<td class="icon"><img src="${favIconUrl}"/></td>
<td class="title">${this.value}: ${tab.title}</td>
<td class="content"><a class="url" target="_blank" href=${
tab.url
}>${tab.url}</a></td>
</tr>`
}
}
export class BufferAllCompletionSource extends Completions.CompletionSourceFuse {
public options: BufferAllCompletionOption[]
constructor(private _parent) {
super(["bufferall "], "BufferAllCompletionSource", "All Buffers")
this.updateOptions()
this._parent.appendChild(this.node)
}
async onInput(exstr) {
await this.updateOptions()
}
private async updateOptions(exstr?: string) {
const tabs: browser.tabs.Tab[] = await Messaging.message(
"commandline_background",
"allWindowTabs",
)
const options = []
tabs.sort((a, b) => {
if (a.windowId == b.windowId) return a.index - b.index
return a.windowId - b.windowId
})
// Window Ids don't make sense so we're using LASTID and WININDEX to compute a window index
// This relies on the fact that tabs are sorted by window ids
let lastId = 0
let winindex = 0
for (const tab of tabs) {
if (lastId != tab.windowId) {
lastId = tab.windowId
winindex += 1
}
options.push(
new BufferAllCompletionOption(
tab.id.toString(),
tab,
winindex,
await Containers.getFromId(tab.cookieStoreId),
),
)
}
this.options = options
this.updateChain()
}
setStateFromScore(scoredOpts: Completions.ScoredOption[]) {
super.setStateFromScore(scoredOpts, true)
}
}

122
src/completions/History.ts Normal file
View file

@ -0,0 +1,122 @@
import * as Completions from "../completions"
import * as config from "../config"
import { browserBg } from "../lib/webext"
class HistoryCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public value: string, page: browser.history.HistoryItem) {
super()
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
// Create HTMLElement
// need to download favicon
const favIconUrl = Completions.DEFAULT_FAVICON
// const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
this.html = html`<tr class="HistoryCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td class="icon"></td>
<td class="title">${page.title}</td>
<td class="content"><a class="url" target="_blank" href=${
page.url
}>${page.url}</a></td>
</tr>`
}
}
export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
public options: HistoryCompletionOption[]
constructor(private _parent) {
super(
["open ", "tabopen ", "winopen "],
"HistoryCompletionSource",
"History",
)
this._parent.appendChild(this.node)
}
public async filter(exstr: string) {
this.lastExstr = exstr
const [prefix, query] = this.splitOnPrefix(exstr)
// Hide self and stop if prefixes don't match
if (prefix) {
// Show self if prefix and currently hidden
if (this.state === "hidden") {
this.state = "normal"
}
} else {
this.state = "hidden"
return
}
this.options = (await this.scoreOptions(query, 10)).map(
page => new HistoryCompletionOption(page.url, page),
)
this.updateChain()
}
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) {
const newtab = browser.runtime.getManifest()["chrome_url_overrides"]
.newtab
const newtaburl = browser.extension.getURL(newtab)
if (!query) {
return (await browserBg.topSites.get())
.filter(page => page.url !== newtaburl)
.slice(0, n)
} else {
// Search history, dedupe and sort by frecency
let history = await browserBg.history.search({
text: query,
maxResults: Number(config.get("historyresults")),
startTime: 0,
})
// Remove entries with duplicate URLs
const dedupe = new Map()
for (const page of history) {
if (page.url !== newtaburl) {
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)
}
}
}

View file

@ -104,7 +104,7 @@ const DEFAULTS = o({
// "n": "findnext 1",
// "N": "findnext -1",
M: "gobble 1 quickmark",
// "B": "fillcmdline bufferall",
B: "fillcmdline bufferall",
b: "fillcmdline buffer",
ZZ: "qall",
f: "hint",
@ -123,6 +123,7 @@ const DEFAULTS = o({
";;": "hint -;",
";#": "hint -#",
";v": "hint -W exclaim_quiet mpv",
";w": "hint -w",
"<S-Insert>": "mode ignore",
"<CA-Esc>": "mode ignore",
"<CA-`>": "mode ignore",
@ -134,8 +135,6 @@ const DEFAULTS = o({
zo: "zoom -0.1 true",
zz: "zoom 1",
".": "repeat",
gow: "open http://www.bbc.co.uk/news/live/uk-44167290",
gnw: "tabopen http://www.bbc.co.uk/news/live/uk-44167290",
"<SA-ArrowUp><SA-ArrowUp><SA-ArrowDown><SA-ArrowDown><SA-ArrowLeft><SA-ArrowRight><SA-ArrowLeft><SA-ArrowRight>ba":
"open https://www.youtube.com/watch?v=M3iOROuTuMA",
}),
@ -143,6 +142,9 @@ const DEFAULTS = o({
DocStart: o({
// "addons.mozilla.org": "mode ignore",
}),
DocEnd: o({
// "emacs.org": "sanitise history",
}),
TriStart: o({
".*": "source_quiet",
}),
@ -234,6 +236,7 @@ const DEFAULTS = o({
homepages: [],
hintchars: "hjklasdfgyuiopqwertnmzxcvb",
hintfiltermode: "simple", // "simple", "vimperator", "vimperator-reflow"
hintnames: "short",
// Controls whether the page can focus elements for you via js
// Remember to also change browser.autofocus (autofocusing elements via
@ -272,6 +275,7 @@ const DEFAULTS = o({
messaging: 2,
cmdline: 2,
controller: 2,
containers: 2,
hinting: 2,
state: 2,
excmd: 1,
@ -298,6 +302,9 @@ const DEFAULTS = o({
// If enabled, tabopen opens a new tab in the currently active tab's container.
tabopencontaineraware: "false",
// If moodeindicator is enabled, containerindicator will color the border of the mode indicator with the container color.
containerindicator: "true",
// Performance related settings
// number of most recent results to ask Firefox for. We display the top 20 or so most frequently visited ones.
@ -475,9 +482,15 @@ async function init() {
// Listen for changes to the storage and update the USERCONFIG if appropriate.
// TODO: BUG! Sync and local storage are merged at startup, but not by this thing.
browser.storage.onChanged.addListener((changes, areaname) => {
browser.storage.onChanged.addListener(async (changes, areaname) => {
if (CONFIGNAME in changes) {
// newValue is undefined when calling browser.storage.AREANAME.clear()
if (changes[CONFIGNAME].newValue !== undefined) {
USERCONFIG = changes[CONFIGNAME].newValue
} else if (areaname === (await get("storageloc"))) {
// If newValue is undefined and AREANAME is the same value as STORAGELOC, the user wants to clean their config
USERCONFIG = o({})
}
}
})

View file

@ -83,6 +83,9 @@ if (
config.getAsync("modeindicator").then(mode => {
if (mode !== "true") return
// Do we want container indicators?
let containerIndicator = config.get("containerindicator")
// Hide indicator in print mode
// CSS not explicitly added to the dom doesn't make it to print mode:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1448507
@ -100,6 +103,22 @@ config.getAsync("modeindicator").then(mode => {
: ""
statusIndicator.className =
"cleanslate TridactylStatusIndicator " + privateMode
// Dynamically sets the border container color.
if (containerIndicator === "true") {
webext
.activeTabContainer()
.then(container => {
statusIndicator.setAttribute(
"style",
`border: ${container.colorCode} solid 1.5px !important`,
)
})
.catch(error => {
logger.debug(error)
})
}
// This listener makes the modeindicator disappear when the mouse goes over it
statusIndicator.addEventListener("mouseenter", ev => {
let target = ev.target as any

View file

@ -79,6 +79,22 @@ export const potentialRules = {
show: ``,
},
},
// All children except add-on panels
navbarnonaddonchildren: {
name: `:root:not([customizing]) #nav-bar > :not(#customizationui-widget-panel)`,
options: {
hide: `display: none !important;`,
show: ``,
},
},
// Set navbar height to 0
navbarnoheight: {
name: `:root:not([customizing]) #nav-bar`,
options: {
hide: ``,
show: `max-height: 0; min-height: 0 !important;`,
},
},
// This inherits transparency if we aren't careful
menubar: {
name: `#navigator-toolbox:not(:hover):not(:focus-within) #toolbar-menubar > *`,
@ -155,11 +171,22 @@ export const metaRules = {
navbarunfocused: "hide",
navtoolboxunfocused: "hide",
navbarafter: "hide",
navbarnonaddonchildren: "show",
navbarnoheight: "hide",
},
always: {
navbarunfocused: "show",
navtoolboxunfocused: "show",
navbarafter: "show",
navbarnonaddonchildren: "show",
navbarnoheight: "hide",
},
none: {
navbarunfocused: "show",
navtoolboxunfocused: "show",
navbarafter: "hide",
navbarnonaddonchildren: "hide",
navbarnoheight: "show",
},
},
}

View file

@ -264,6 +264,26 @@ export function getAllDocumentFrames(doc = document) {
)
}
/** Computes the unique CSS selector of a specific HTMLElement */
export function getSelector(e: HTMLElement) {
function uniqueSelector(e: HTMLElement) {
// Only matching alphanumeric selectors because others chars might have special meaning in CSS
if (e.id && e.id.match("^[a-zA-Z0-9]+$")) return "#" + e.id
// If we reached the top of the document
if (!e.parentElement) return "HTML"
// Compute the position of the element
let index =
Array.from(e.parentElement.children)
.filter(child => child.tagName == e.tagName)
.indexOf(e) + 1
return (
uniqueSelector(e.parentElement) +
` > ${e.tagName}:nth-of-type(${index})`
)
}
return uniqueSelector(e)
}
/** Get all elements that match the given selector
*
* @param selector `the CSS selector to choose elements with
@ -426,11 +446,21 @@ export function hijackPageListenerFunctions(): void {
/** Focuses an input element and makes sure the cursor is put at the end of the input */
export function focus(e: HTMLElement): void {
e.focus()
if (e instanceof HTMLInputElement) {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange
// "Note that accordingly to the WHATWG forms spec selectionStart,
// selectionEnd properties and setSelectionRange method apply only to
// inputs of types text, search, URL, tel and password"
// So you can't put the cursor at the end of an email field. I can't
// believe how stupid this is.
if (
e instanceof HTMLInputElement &&
["text", "search", "url", "tel", "password"].includes(
e.type.toLowerCase(),
)
) {
let pos = 0
if (config.get("cursorpos") === "end") pos = e.value.length
e.selectionStart = pos
e.selectionEnd = e.selectionStart
e.setSelectionRange(pos, pos)
}
}

View file

@ -88,6 +88,7 @@
// Shared
import * as Messaging from "./messaging"
import { l, browserBg, activeTabId, activeTabContainerId } from "./lib/webext"
import * as Container from "./lib/containers"
import state from "./state"
import * as UrlUtil from "./url_util"
import * as config from "./config"
@ -139,13 +140,14 @@ export async function getNativeVersion(): Promise<void> {
}
/**
* Fills the last used input box with content. You probably don't want this; it's used internally for [[editor]].
* 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's used internally for [[editor]].
*
* That said, `bind gs fillinput [Tridactyl](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/) is my favourite add-on` could probably come in handy.
* 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 async function fillinput(...content: string[]) {
let inputToFill = DOM.getLastUsedInput()
export async function fillinput(selector: string, ...content: string[]) {
let inputToFill = document.querySelector(selector)
if (!inputToFill) inputToFill = DOM.getLastUsedInput()
if ("value" in inputToFill) {
;(inputToFill as HTMLInputElement).value = content.join(" ")
} else {
@ -165,6 +167,12 @@ export async function getinput() {
}
}
/** @hidden */
//#content
export async function getInputSelector() {
return DOM.getSelector(DOM.getLastUsedInput())
}
/**
* 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 [[installnative]]**.
@ -177,10 +185,13 @@ export async function getinput() {
*/
//#background
export async function editor() {
let url = new URL((await activeTab()).url)
let tab = await activeTab()
let selector = await Messaging.messageTab(tab.id, "excmd_content", "getInputSelector", [])
let url = new URL(tab.url)
if (!await Native.nativegate()) return
const file = (await Native.temp(await getinput(), url.hostname)).content
fillinput((await Native.editor(file)).content)
// We're using Messaging.messageTab instead of `fillinput()` because fillinput() will execute in the currently active tab, which might not be the tab the user spawned the editor in
Messaging.messageTab(tab.id, "excmd_content", "fillinput", [selector, (await Native.editor(file)).content])
// TODO: add annoying "This message was written with [Tridactyl](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/)"
// to everything written using editor
}
@ -210,6 +221,7 @@ import * as css_util from "./css_util"
* - navbar
* - always
* - autohide
* - none
*
* - hoverlink (the little link that appears when you hover over a link)
* - none
@ -272,7 +284,7 @@ export function cssparse(...css: string[]) {
* ```
* in about:config via user.js so that Tridactyl (and other extensions!) can be used on addons.mozilla.org and other sites.
*
* Requires `native`.
* Requires `native` and a `restart`.
*/
//#background
export async function fixamo() {
@ -290,13 +302,34 @@ export async function fixamo() {
//#background
export async function nativeopen(url: string, ...firefoxArgs: string[]) {
if (await Native.nativegate()) {
// First compute where the tab should be
let pos = await config.getAsync("tabopenpos")
let index = (await activeTab()).index + 1
switch (pos) {
case "last":
index = 99999
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
let selecttab = tab => {
browser.tabs.onCreated.removeListener(selecttab)
tabSetActive(tab.id)
browser.tabs.move(tab.id, { index })
}
browser.tabs.onCreated.addListener(selecttab)
if ((await browser.runtime.getPlatformInfo()).os === "mac") {
let osascriptArgs = ["-e 'on run argv'", "-e 'tell application \"Firefox\" to open location item 1 of argv'", "-e 'end run'"]
Native.run("osascript " + osascriptArgs.join(" ") + " " + url)
await Native.run("osascript " + osascriptArgs.join(" ") + " " + url)
} else {
if (firefoxArgs.length === 0) firefoxArgs = ["--new-tab"]
Native.run(config.get("browser") + " " + firefoxArgs.join(" ") + " " + url)
await Native.run(config.get("browser") + " " + firefoxArgs.join(" ") + " " + url)
}
setTimeout(() => browser.tabs.onCreated.removeListener(selecttab), 100)
}
}
@ -642,9 +675,7 @@ document.addEventListener("scroll", addJump)
// Try to restore the previous jump position every time a page is loaded
//#content_helper
curJumps().then(() => {
jumpprev(0)
})
document.addEventListener("load", () => curJumps().then(() => jumpprev(0)))
/** Blur (unfocus) the active element */
//#content
@ -796,8 +827,9 @@ export const ABOUT_WHITELIST = ["about:home", "about:license", "about:logo", "ab
* - else treat as search parameters for google
*
* Related settings:
* "searchengine": "google" or any of [[SEARCH_URLS]]
* "historyresults": the n-most-recent results to ask Firefox for before they are sorted by frequency. Reduce this number if you find your results are bad.
* - "searchengine": "google" or any of [[SEARCH_URLS]]
* - "historyresults": the n-most-recent results to ask Firefox for before they are sorted by frequency. Reduce this number if you find your results are bad.
*
* 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.
*
*/
@ -1032,7 +1064,12 @@ export function urlincrement(count = 1) {
let newUrl = UrlUtil.incrementUrl(window.location.href, count)
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}`)
}
}
}
@ -1221,9 +1258,12 @@ export async function reader() {
//#content_helper
loadaucmds("DocStart")
//#content_helper
window.addEventListener("pagehide", () => loadaucmds("DocEnd"))
/** @hidden */
//#content
export async function loadaucmds(cmdType: "DocStart" | "TabEnter" | "TabLeft") {
export async function loadaucmds(cmdType: "DocStart" | "DocEnd" | "TabEnter" | "TabLeft") {
let aucmds = await config.getAsync("autocmds", cmdType)
const ausites = Object.keys(aucmds)
const aukeyarr = ausites.filter(e => window.document.location.href.search(e) >= 0)
@ -1446,7 +1486,9 @@ export async function tablast() {
/** 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 `-b` flag as the first argument to open the tab in the background.
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.
Use the `-b` flag to open the tab in the background.
These two can be combined in any order, but need to be placed as the first arguments.
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
@ -1464,13 +1506,29 @@ export async function tablast() {
//#background
export async function tabopen(...addressarr: string[]) {
let active
if (addressarr[0] === "-b") {
addressarr.shift()
let container
// Lets us pass both -b and -c in no particular order as long as they are up front.
async function argParse(args): Promise<string[]> {
if (args[0] === "-b") {
active = false
args.shift()
argParse(args)
} else if (args[0] === "-c") {
// Ignore the -c flag if incognito as containers are disabled.
let win = await browser.windows.getCurrent()
if (!win["incognito"]) 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
}
let url: string
let address = addressarr.join(" ")
let address = (await argParse(addressarr)).join(" ")
if (!ABOUT_WHITELIST.includes(address) && address.match(/^(about|file):.*/)) {
if ((await browser.runtime.getPlatformInfo()).os === "mac" && (await browser.windows.getCurrent()).incognito) {
@ -1484,7 +1542,9 @@ export async function tabopen(...addressarr: string[]) {
else url = forceURI(config.get("newtab"))
activeTabContainerId().then(containerId => {
if (containerId && config.get("tabopencontaineraware") === "true") openInNewTab(url, { active: active, cookieStoreId: containerId })
// Ensure -c has priority.
if (container) openInNewTab(url, { active: active, cookieStoreId: container })
else if (containerId && config.get("tabopencontaineraware") === "true") openInNewTab(url, { active: active, cookieStoreId: containerId })
else openInNewTab(url, { active })
})
}
@ -1723,6 +1783,92 @@ export async function qall() {
// }}}
// {{{ CONTAINERS
/** Closes all tabs open in the same container across all windows.
@param name The container name.
*/
//#background
export async function containerclose(name: string) {
let containerId = await Container.getId(name)
browser.tabs.query({ cookieStoreId: containerId }).then(tabs => {
browser.tabs.remove(
tabs.map(tab => {
return 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 containerremove(name: string) {
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)
try {
let containerId = await Container.fuzzyMatch(name)
let containerObj = Container.fromString(uname, ucolor, uicon)
await Container.update(containerId, containerObj)
} catch (e) {
throw e
}
}
/** 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.
let containers = await browserBg.contextualIdentities.query({}) // Can't access src/lib/containers.ts from a content script.
window.location.href =
"data:application/json," +
JSON.stringify(containers)
.replace(/#/g, "%23")
.replace(/ /g, "%20")
}
// }}}
//
// {{{ MISC
/** Deprecated
@ -1953,15 +2099,37 @@ export async function clipboard(excmd: "open" | "yank" | "yankshort" | "yankcano
/** Change active tab.
@param index
Starts at 1. 0 refers to last tab, -1 to penultimate tab, etc.
Starts at 1. 0 refers to last tab of the current window, -1 to penultimate tab, etc.
"#" means the tab that was last accessed in this window
This is different from [[bufferall]] because `index` is the position of the tab in the window.
*/
//#background
export async function buffer(index: number | "#") {
tabIndexSetActive(index)
}
/** Change active tab.
@param id
A string following the following format: "[0-9]+.[0-9]+", the first number being the index of the window that should be selected and the second one being the index of the tab within that window.
*/
//#background
export async function bufferall(id: string) {
let windows = (await browser.windows.getAll()).map(w => w.id).sort()
if (id === null || id === undefined || !id.match(/\d+\.\d+/)) {
const tab = await activeTab()
let prevId = id
id = windows.indexOf(tab.windowId) + "." + (tab.index + 1)
logger.info(`bufferall: Bad tab id: ${prevId}, defaulting to ${id}`)
}
let [winindex, tabindex] = id.split(".")
await browser.windows.update(windows[parseInt(winindex) - 1], { focused: true })
return browser.tabs.update(await idFromIndex(tabindex), { active: true })
}
// }}}
// }}}
@ -2124,9 +2292,9 @@ export function set(key: string, ...values: string[]) {
/** Set autocmds to run when certain events happen.
@param event Curently, 'TriStart', 'DocStart', 'TabEnter' and 'TabLeft' are supported.
@param event Curently, 'TriStart', 'DocStart', 'DocEnd', 'TabEnter' and 'TabLeft' are supported.
@param url For DocStart, TabEnter, and TabLeft: a fragment of the URL on which the events will trigger, or a JavaScript regex (e.g, `/www\.amazon\.co.*\/`)
@param url For DocStart, DocEnd, TabEnter, and TabLeft: a fragment of the URL on which the events will trigger, or 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).
@ -2142,7 +2310,7 @@ export function set(key: string, ...values: string[]) {
export function autocmd(event: string, url: string, ...excmd: string[]) {
// rudimentary run time type checking
// TODO: Decide on autocmd event names
if (!["DocStart", "TriStart", "TabEnter", "TabLeft"].includes(event)) throw event + " is not a supported event."
if (!["DocStart", "DocEnd", "TriStart", "TabEnter", "TabLeft"].includes(event)) throw event + " is not a supported event."
config.set("autocmds", event, url, excmd.join(" "))
}
@ -2420,9 +2588,24 @@ import * as hinting from "./hinting_background"
To open a hint in the background, the default bind is `F`.
Related settings:
"hintchars": "hjklasdfgyuiopqwertnmzxcvb"
"hintfiltermode": "simple" | "vimperator" | "vimperator-reflow"
"relatedopenpos": "related" | "next" | "last"
- "hintchars": "hjklasdfgyuiopqwertnmzxcvb"
- "hintfiltermode": "simple" | "vimperator" | "vimperator-reflow"
- "relatedopenpos": "related" | "next" | "last"
- "hintnames": "short" | "uniform" | "numeric"
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).
*/
//#background
export function hint(option?: string, selectors?: string, ...rest: string[]) {

View file

@ -119,12 +119,21 @@ function defaultHintFilter() {
}
}
function defaultHintChars() {
switch (config.get("hintnames")) {
case "numeric":
return "1234567890"
default:
return config.get("hintchars")
}
}
/** An infinite stream of hints
Earlier hints prefix later hints
*/
function* hintnames_simple(
hintchars = config.get("hintchars"),
hintchars = defaultHintChars(),
): IterableIterator<string> {
for (let taglen = 1; true; taglen++) {
yield* map(permutationsWithReplacement(hintchars, taglen), e =>
@ -147,9 +156,9 @@ function* hintnames_simple(
and so on, but we hardly ever see that many hints, so whatever.
*/
function* hintnames(
function* hintnames_short(
n: number,
hintchars = config.get("hintchars"),
hintchars = defaultHintChars(),
): IterableIterator<string> {
let source = hintnames_simple(hintchars)
const num2skip = Math.floor(n / hintchars.length)
@ -159,7 +168,7 @@ function* hintnames(
/** Uniform length hintnames */
function* hintnames_uniform(
n: number,
hintchars = config.get("hintchars"),
hintchars = defaultHintChars(),
): IterableIterator<string> {
if (n <= hintchars.length) yield* islice(hintchars[Symbol.iterator](), n)
else {
@ -175,6 +184,26 @@ function* hintnames_uniform(
}
}
function* hintnames_numeric(n: number): IterableIterator<string> {
for (let i = 1; i <= n; i++) {
yield String(i)
}
}
function* hintnames(
n: number,
hintchars = defaultHintChars(),
): IterableIterator<string> {
switch (config.get("hintnames")) {
case "numeric":
yield* hintnames_numeric(n)
case "uniform":
yield* hintnames_uniform(n, hintchars)
default:
yield* hintnames_short(n, hintchars)
}
}
type HintSelectedCallback = (Hint) => any
/** Place a flag by each hintworthy element */
@ -256,9 +285,7 @@ function buildHintsVimperator(els: Element[], onSelect: HintSelectedCallback) {
let names = hintnames(els.length)
// escape the hintchars string so that strange things don't happen
// when special characters are used as hintchars (for example, ']')
const escapedHintChars = config
.get("hintchars")
.replace(/^\^|[-\\\]]/g, "\\$&")
const escapedHintChars = defaultHintChars().replace(/^\^|[-\\\]]/g, "\\$&")
const filterableTextFilter = new RegExp("[" + escapedHintChars + "]", "g")
for (let [el, name] of izip(els, names)) {
let ft = elementFilterableText(el)
@ -322,7 +349,7 @@ function filterHintsVimperator(fstr, reflow = false) {
/** Partition a fstr into a tagged array of substrings */
function partitionFstr(fstr): { str: string; isHintChar: boolean }[] {
const peek = a => a[a.length - 1]
const hintChars = config.get("hintchars")
const hintChars = defaultHintChars()
// For each char, either add it to the existing run if there is one and
// it's a matching type or start a new run

244
src/lib/containers.ts Normal file
View file

@ -0,0 +1,244 @@
import { browserBg } from "./webext"
import * as Logging from "../logging"
const logger = new Logging.Logger("containers")
// As per Mozilla specification: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/contextualIdentities/ContextualIdentity
const ContainerColor = [
"blue",
"turquoise",
"green",
"yellow",
"orange",
"red",
"pink",
"purple",
]
// As per Mozilla specification: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/contextualIdentities/ContextualIdentity
const ContainerIcon = [
"fingerprint",
"briefcase",
"dollar",
"cart",
"circle",
"gift",
"vacation",
"food",
"fruit",
"pet",
"tree",
"chill",
]
const DefaultContainer = Object.freeze(
fromString("default", "invisible", "noicond", "firefox-default"),
)
/** Creates a container from the specified parameters.Does not allow multiple containers with the same name.
@param name The container name.
@param color The container color, must be one of: "blue", "turquoise", "green", "yellow", "orange", "red", "pink" or "purple". If nothing is supplied, it selects one at random.
@param icon The container icon, must be one of: "fingerprint", "briefcase", "dollar", "cart", "circle", "gift", "vacation", "food", "fruit", "pet", "tree", "chill"
*/
export async function create(
name: string,
color = "random",
icon = "fingerprint",
): Promise<string> {
if (color === "random") color = chooseRandomColor()
let container = fromString(name, color, icon)
logger.debug(container)
if (await exists(name)) {
logger.debug(`[Container.create] container already exists ${container}`)
throw new Error(
`[Container.create] container already exists, aborting.`,
)
} else {
try {
let res = await browser.contextualIdentities.create(container)
logger.info(
"[Container.create] created container:",
res["cookieStoreId"],
)
return res["cookieStoreId"]
} catch (e) {
throw e
}
}
}
/** Removes specified container. No fuzzy matching is intentional here. If there are multiple containers with the same name (allowed by other container plugins), it chooses the one with the lowest cookieStoreId
@param name The container name
*/
export async function remove(name: string) {
logger.debug(name)
try {
let id = await getId(name)
let res = await browser.contextualIdentities.remove(id)
logger.debug("[Container.remove] removed container:", res.cookieStoreId)
} catch (e) {
throw e
}
}
/** Updates the specified container.
TODO: pass an object to this when tridactyl gets proper flag parsing
NOTE: while browser.contextualIdentities.create does check for valid color/icon combos, browser.contextualIdentities.update does not.
@param containerId Expects a cookieStringId e.g. "firefox-container-n".
@param name the new name of the container
@param color the new color of the container
@param icon the new icon of the container
*/
export async function update(
containerId: string,
updateObj: {
name: string
color: browser.contextualIdentities.IdentityColor
icon: browser.contextualIdentities.IdentityIcon
},
) {
if (isValidColor(updateObj["color"]) && isValidIcon(updateObj["icon"])) {
try {
browser.contextualIdentities.update(containerId, updateObj)
} catch (e) {
throw e
}
} else {
logger.debug(updateObj)
throw new Error("[Container.update] invalid container icon or color")
}
}
/** Gets a container object from a supplied container id string. If no container corresponds to containerId, returns a default empty container.
@param containerId Expects a cookieStringId e.g. "firefox-container-n"
*/
export async function getFromId(
containerId: string,
): Promise<browser.contextualIdentities.ContextualIdentity> {
try {
return await browserBg.contextualIdentities.get(containerId)
} catch (e) {
return DefaultContainer
}
}
/** Fetches all containers from Firefox's contextual identities API and checks if one exists with the specified name.
Note: This operation is entirely case-insensitive.
@param string cname
@returns boolean Returns true when cname matches an existing container or on query error.
*/
export async function exists(cname: string): Promise<boolean> {
let exists = false
try {
let containers = await getAll()
let res = containers.filter(c => {
return c.name.toLowerCase() === cname.toLowerCase()
})
if (res.length > 0) {
exists = true
}
} catch (e) {
exists = true // Make sure we don't accidentally break the constraint on query error.
logger.error(
"[Container.exists] Error querying contextualIdentities:",
e,
)
}
return exists
}
/** Takes string parameters and returns them as a pseudo container object
for use in other functions in the library.
@param name
@param color
@param icon
*/
export function fromString(
name: string,
color: string,
icon: string,
id: string = "",
) {
try {
return {
name: name,
color: color as browser.contextualIdentities.IdentityColor,
icon: icon as browser.contextualIdentities.IdentityIcon,
cookieStoreId: id,
} as browser.contextualIdentities.ContextualIdentity // rules are made to be broken
} catch (e) {
throw e
}
}
/**
* @returns An array representation of all containers.
*/
export async function getAll(): Promise<any[]> {
return await browser.contextualIdentities.query({})
}
/**
* @param name The container name
* @returns The cookieStoreId of the first match of the query.
*/
export async function getId(name: string): Promise<string> {
try {
return (await browser.contextualIdentities.query({ name: name }))[0][
"cookieStoreId"
]
} catch (e) {
throw new Error(
"[Container.getId] could not find a container with that name.",
)
}
}
/** Tries some simple ways to match containers to your input.
Fuzzy matching is entirely case-insensitive.
@param partialName The (partial) name of the container.
*/
export async function fuzzyMatch(partialName: string): Promise<string> {
let containers = await getAll()
let exactMatch = containers.filter(c => {
return c.name.toLowerCase() === partialName.toLowerCase()
})
if (exactMatch.length === 1) {
return exactMatch[0]["cookieStoreId"]
} else if (exactMatch.length > 1) {
throw new Error(
"[Container.fuzzyMatch] more than one container with this name exists.",
)
} else {
let fuzzyMatches = containers.filter(c => {
return c.name.toLowerCase().indexOf(partialName.toLowerCase()) > -1
})
if (fuzzyMatches.length === 1) {
return fuzzyMatches[0]["cookieStoreId"]
} else if (fuzzyMatches.length > 1) {
throw new Error(
"[Container.fuzzyMatch] ambiguous match, provide more characters",
)
} else {
throw new Error(
"[Container.fuzzyMatch] no container matched that string",
)
}
}
}
/** Helper function for create, returns a random valid IdentityColor for use if no color is applied at creation.*/
function chooseRandomColor(): string {
let max = Math.floor(ContainerColor.length)
let n = Math.floor(Math.random() * max)
return ContainerColor[n]
}
function isValidColor(color: string): boolean {
return ContainerColor.indexOf(color) > -1
}
function isValidIcon(icon: string): boolean {
return ContainerIcon.indexOf(icon) > -1
}

View file

@ -72,6 +72,17 @@ export async function activeTabContainerId() {
return (await activeTab()).cookieStoreId
}
//#background_helper
export async function activeTabContainer() {
let containerId = await activeTabContainerId()
if (containerId !== "firefox-default")
return await browserBg.contextualIdentities.get(containerId)
else
throw new Error(
"firefox-default is not a valid contextualIdentity (activeTabContainer)",
)
}
/** Compare major firefox versions */
export async function firefoxVersionAtLeast(desiredmajor: number) {
const versionstr = (await browserBg.runtime.getBrowserInfo()).version

View file

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

37
src/newtab.ts Normal file
View file

@ -0,0 +1,37 @@
// This file is only included in newtab.html, after content.js has been loaded
// These functions work with the elements created by tridactyl/scripts/newtab.md.sh
function getChangelogDiv() {
const changelogDiv = document.getElementById("changelog")
if (!changelogDiv) throw new Error("Couldn't find changelog element!")
return changelogDiv
}
function updateChangelogStatus() {
const changelogDiv = getChangelogDiv()
const changelogContent = changelogDiv.textContent
if (localStorage.changelogContent == changelogContent) {
const changelogButton = document.querySelector('input[id^="spoiler"]')
if (!changelogButton) {
console.error("Couldn't find changelog button!")
return
}
changelogButton.classList.add("seen")
}
}
function readChangelog() {
const changelogDiv = getChangelogDiv()
localStorage.changelogContent = changelogDiv.textContent
updateChangelogStatus()
}
window.addEventListener("load", updateChangelogStatus)
window.addEventListener("load", _ => {
const spoilerbutton = document.getElementById("spoilerbutton")
if (!spoilerbutton) {
console.error("Couldn't find spoiler button!")
return
}
spoilerbutton.addEventListener("click", readChangelog)
})

View file

@ -18,4 +18,4 @@ We support a handful of keybinds in the console:
* `Ctrl-F` to complete the command from command history
* `Space` to insert the URL of the highlighted completion into the command line
The [next page](./settings.html) will talk about the various settings available.
The [next page](./settings.html) will talk about the various settings available. <a href='./hint_mode.html' rel="prev"></a>

View file

@ -8,4 +8,4 @@ You can get help about any command by typing `help [command]` in command mode. A
Lastly, you can contact the developers via Matrix or GitHub, as mentioned on the new tab page.
This concludes the tutorial. If you have any feedback, please leave it on [the relevant GitHub issue](https://github.com/cmcaine/tridactyl/issues/380).
This concludes the tutorial. If you have any feedback, please leave it on [the relevant GitHub issue](https://github.com/cmcaine/tridactyl/issues/380). <a href='./settings.html' rel="prev"></a>

View file

@ -12,4 +12,4 @@ Here are some of the most useful hint modes:
If there is ever only a single hint remaining (for example, because you have wittled them down, or there is only a single link visible on the page) the hint mode will follow it automatically.
The [next page](./command_mode.html) will cover the command mode.
The [next page](./command_mode.html) will cover the command mode. <a href='./normal_mode.html' rel="prev"></a>

View file

@ -29,3 +29,5 @@ Many keypresses in normal mode take you into another mode. `t`, for example, put
All the keys in normal mode are bound to commands; for example, `j` is bound to `scrolline 10`. If you are ever curious as to what a key sequence does in normal mode, you can simply use `:bind [keys]` and the command line will tell you to which command they are bound.
The [next page](./hint_mode.html) will explain how to use some of the various hint modes. This time try `]]` (guess the next page) to follow the link.
<a href='./tutor.html' rel="prev"></a>

View file

@ -18,4 +18,4 @@ Here we will briefly summarise some of the main settings:
* excmds
* aliases for command mode: the things on the left actually run the commands on the right. The most interesting one of these is `current_url`, which is how the binds for O, W and T (`bind T`) work.
The <a href='./help.html' rel='next'>final page</a> describes how you can get further help.
The <a href='./help.html' rel='next'>final page</a> describes how you can get further help. <a href='./command_mode.html' rel="prev"></a>

View file

@ -4,7 +4,7 @@ Hello. If you've just installed Tridactyl for the first time, welcome! Tridactyl
Welcome to the Tridactyl tutorial. Here, you will learn how to get started with this extension. If you ever want to get back to this page, just type `:tutor`.
It will not cover advanced topics. For those, [`:help`](../docs/modules/_excmds_.html) is always at hand.
It will not cover advanced topics. For those, [`:help`](../docs/modules/_src_excmds_.html) is always at hand.
---

View file

@ -51,53 +51,29 @@ input {
}
/* Olie doesn't know how CSS inheritance works */
#completions .HistoryCompletionSource {
#completions > div {
max-height: calc(20 * var(--option-height));
min-height: calc(10 * var(--option-height));
}
#completions .HistoryCompletionSource table {
#completions > div > table {
width: 100%;
font-size: 9pt;
border-spacing: 0;
table-layout: fixed;
}
/* redundancy 2: redundancy 2: more redundancy */
#completions .BmarkCompletionSource {
max-height: calc(20 * var(--option-height));
min-height: calc(10 * var(--option-height));
}
#completions .BmarkCompletionSource table {
width: 100%;
font-size: 9pt;
border-spacing: 0;
table-layout: fixed;
}
/* redundancy ends */
#completions .BufferCompletionSource {
max-height: calc(20 * var(--option-height));
min-height: calc(10 * var(--option-height));
}
#completions .BufferCompletionSource table {
width: 100%;
font-size: 9pt;
border-spacing: 0;
table-layout: fixed;
}
#completions table tr td:nth-of-type(1) {
width: 1.5em;
#completions table tr td.prefix {
width: 1em;
padding-left: 0.5em;
text-align: center;
}
#completions table tr td:nth-of-type(2) {
#completions table tr td.icon,
#completions table tr td.container {
width: 1.5em;
}
/* #completions table tr td:nth-of-type(3) { width: 5em; } */
#completions table tr td:nth-of-type(4) {
#completions table tr td.content {
width: 50%;
}
@ -171,3 +147,71 @@ a.url:hover {
color: var(--tridactyl-of-fg);
background: var(--tridactyl-of-bg);
}
/* Still completions, but container-related stuff */
.option .container {
background-size: 1em;
background-repeat: no-repeat;
background-position: center;
-moz-context-properties: fill;
}
.option.container_blue .container {
fill: var(--tridactyl-container-color-blue);
}
.option.container_turquoise .container {
fill: var(--tridactyl-container-color-turquoise);
}
.option.container_green .container {
fill: var(--tridactyl-container-color-green);
}
.option.container_yellow .container {
fill: var(--tridactyl-container-color-yellow);
}
.option.container_orange .container {
fill: var(--tridactyl-container-color-orange);
}
.option.container_red .container {
fill: var(--tridactyl-container-color-red);
}
.option.container_pink .container {
fill: var(--tridactyl-container-color-pink);
}
.option.container_purple .container {
fill: var(--tridactyl-container-color-purple);
}
.option.container_fingerprint .container {
background-image: var(--tridactyl-container-fingerprint-url);
}
.option.container_briefcase .container {
background-image: var(--tridactyl-container-briefcase-url);
}
.option.container_dollar .container {
background-image: var(--tridactyl-container-dollar-url);
}
.option.container_cart .container {
background-image: var(--tridactyl-container-cart-url);
}
.option.container_circle .container {
background-image: var(--tridactyl-container-circle-url);
}
.option.container_gift .container {
background-image: var(--tridactyl-container-gift-url);
}
.option.container_vacation .container {
background-image: var(--tridactyl-container-vacation-url);
}
.option.container_food .container {
background-image: var(--tridactyl-container-food-url);
}
.option.container_fruit .container {
background-image: var(--tridactyl-container-fruit-url);
}
.option.container_pet .container {
background-image: var(--tridactyl-container-pet-url);
}
.option.container_tree .container {
background-image: var(--tridactyl-container-tree-url);
}
.option.container_chill .container {
background-image: var(--tridactyl-container-chill-url);
}

View file

@ -30,6 +30,15 @@ input[id^="spoiler"]:checked + label {
color: #333;
background: #ccc;
}
input[id^="spoiler"]:checked + label > #nagbar-changelog,
input[id^="spoiler"].seen + label > #nagbar-changelog {
display: none;
}
#nagbar-changelog {
font-size: 8pt;
background: red;
line-height: 1.4;
}
input[id^="spoiler"] ~ .spoiler {
height: 0;
overflow: hidden;

View file

@ -41,8 +41,8 @@ REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED
## Important limitations due to WebExtensions
* You can only navigate to most about:_\file:_ pages if you have Tridactyl's native executable installed.
* Firefox will not load Tridactyl on addons.mozilla.org, about:\*, some file:\* URIs, view-source:\*, or data:\*. On these pages Ctrl-L (or F6), Ctrl-Tab and Ctrl-W are your escape hatches.
* You can only navigate to most about: and file: pages if you have Tridactyl's native executable installed.
* Firefox will not load Tridactyl on about:\*, some file:\* URIs, view-source:\*, or data:\*. On these pages Ctrl-L (or F6), Ctrl-Tab and Ctrl-W are your escape hatches.
* You can change the Firefox GUI with `guiset` (e.g. `guiset gui none` and then `restart`) if you have the native messenger installed, or you can do it yourself by changing your userChrome. There is an example file available on our repository [[2]].
* Tridactyl cannot capture key presses until web pages are loaded. You can use `:reloadall` to reload all tabs to make life more bearable, or flip `browser.sessionstore.restore_tabs_lazily` to false in `about:config`.
@ -66,9 +66,9 @@ You have more questions? Have a look at our [FAQ][faq-link].
[3]: https://www.mozilla.org/en-US/firefox/organizations/
<div class="align-left">
\[1]: https://github.com/cmcaine/tridactyl/issues<br />
\[2]: https://github.com/cmcaine/tridactyl/blob/master/src/static/css/userChrome-minimal.css<br />
\[3]: https://www.mozilla.org/en-US/firefox/organizations/<br />
[1]: https://github.com/cmcaine/tridactyl/issues<br />
[2]: https://github.com/cmcaine/tridactyl/blob/master/src/static/css/userChrome-minimal.css<br />
[3]: https://www.mozilla.org/en-US/firefox/organizations/<br />
</div>
[faq-link]: https://github.com/cmcaine/tridactyl#frequently-asked-questions

View file

@ -13,4 +13,5 @@
REPLACETHIS
</body>
<script src="../content.js"></script>
<script src="../newtab.js"></script>
</html>

View file

@ -96,4 +96,26 @@
/*new tab spoiler box*/
--tridactyl-highlight-box-bg: #eee;
--tridactyl-highlight-box-fg: var(--tridactyl-fg);
--tridactyl-container-fingerprint-url: url("resource://usercontext-content/fingerprint.svg");
--tridactyl-container-briefcase-url: url("resource://usercontext-content/briefcase.svg");
--tridactyl-container-dollar-url: url("resource://usercontext-content/dollar.svg");
--tridactyl-container-cart-url: url("resource://usercontext-content/cart.svg");
--tridactyl-container-circle-url: url("resource://usercontext-content/circle.svg");
--tridactyl-container-gift-url: url("resource://usercontext-content/gift.svg");
--tridactyl-container-vacation-url: url("resource://usercontext-content/vacation.svg");
--tridactyl-container-food-url: url("resource://usercontext-content/food.svg");
--tridactyl-container-fruit-url: url("resource://usercontext-content/fruit.svg");
--tridactyl-container-pet-url: url("resource://usercontext-content/pet.svg");
--tridactyl-container-tree-url: url("resource://usercontext-content/tree.svg");
--tridactyl-container-chill-url: url("resource://usercontext-content/chill.svg");
--tridactyl-container-color-blue: #37adff;
--tridactyl-container-color-turquoise: #00c79a;
--tridactyl-container-color-green: #51cd00;
--tridactyl-container-color-yellow: #ffcb00;
--tridactyl-container-color-orange: #ff9f00;
--tridactyl-container-color-red: #ff613d;
--tridactyl-container-color-pink: #ff4bda;
--tridactyl-container-color-purple: #af51f5;
}

View file

@ -8,6 +8,7 @@ const logger = new Logging.Logger("styling")
const THEMES = ["dark", "greenmat", "shydactyl", "quake"]
function capitalise(str) {
if (str === "") return str
return str[0].toUpperCase() + str.slice(1)
}
@ -61,3 +62,21 @@ browser.storage.onChanged.addListener((changes, areaname) => {
retheme()
}
})
// Sometimes pages will overwrite class names of elements. We use a MutationObserver to make sure that the HTML element always has a TridactylTheme class
// We can't just call theme() because it would first try to remove class names from the element, which would trigger the MutationObserver before we had a chance to add the theme class and thus cause infinite recursion
let cb = async mutationList => {
let theme = await config.getAsync("theme")
mutationList
.filter(m => m.target.className.search(prefixTheme("")) == -1)
.forEach(m => m.target.classList.add(prefixTheme(theme)))
}
new MutationObserver(cb).observe(document.documentElement, {
attributes: true,
childList: false,
characterData: false,
subtree: false,
attributeOldValue: false,
attributeFilter: ["class"],
})

View file

@ -8,6 +8,7 @@ module.exports = {
content: "./src/content.ts",
commandline_frame: "./src/commandline_frame.ts",
help: "./src/help.ts",
newtab: "./src/newtab.ts",
},
output: {
filename: "[name].js",