mirror of
https://github.com/vale981/tridactyl
synced 2025-03-05 09:31:41 -05:00
Merge branch 'feature-webpack-build'
This commit is contained in:
commit
cd9ba01d79
23 changed files with 4320 additions and 576 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
build
|
||||
src/node_modules
|
||||
node_modules
|
||||
|
|
3793
package-lock.json
generated
Normal file
3793
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "tridactyl",
|
||||
"version": "0.1.0",
|
||||
"description": "Vimperator/Pentadactyl successor",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"web-ext-types": "github:michael-zapata/web-ext-types",
|
||||
"typescript": "^2.5.3",
|
||||
"webpack": "^3.6.0",
|
||||
"awesome-typescript-loader": "^3.2.3",
|
||||
"copy-webpack-plugin": "^4.1.0",
|
||||
"source-map-loader": "^0.2.2",
|
||||
"uglify-es": "^3.1.2",
|
||||
"uglifyjs-webpack-plugin": "^1.0.0-beta.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"clean": "rm -rf build",
|
||||
"test": "npm run build; echo; echo No other tests yet"
|
||||
},
|
||||
"author": "Colin Caine"
|
||||
}
|
13
src/background.ts
Normal file
13
src/background.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/** Background script entry point. */
|
||||
|
||||
import * as Controller from "./controller"
|
||||
import * as keydown_background from "./keydown_background"
|
||||
import * as CommandLine from "./commandline_background"
|
||||
|
||||
// Send keys to controller
|
||||
keydown_background.onKeydown.addListener(Controller.acceptKey)
|
||||
// To eventually be replaced by:
|
||||
// browser.keyboard.onKeydown.addListener
|
||||
|
||||
// Send commandline to controller
|
||||
CommandLine.onLine.addListener(Controller.acceptExCmd)
|
|
@ -2,29 +2,27 @@
|
|||
|
||||
Receives messages from CommandLinePage
|
||||
*/
|
||||
namespace CommandLine {
|
||||
export namespace onLine {
|
||||
export namespace onLine {
|
||||
|
||||
type onLineCallback = (exStr: string) => void
|
||||
type onLineCallback = (exStr: string) => void
|
||||
|
||||
const listeners = new Set<onLineCallback>()
|
||||
export function addListener(cb: onLineCallback) {
|
||||
listeners.add(cb)
|
||||
return () => { listeners.delete(cb) }
|
||||
}
|
||||
|
||||
// Receive events from CommandLinePage and pass to listeners
|
||||
function handler(message: any) {
|
||||
if (message.command === "commandline") {
|
||||
for (let listener of listeners) {
|
||||
listener(message.exStr)
|
||||
}
|
||||
}
|
||||
// This is req. to shut typescript up.
|
||||
// TODO: Fix onMessageBool in web-ext-types
|
||||
return false
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(handler)
|
||||
const listeners = new Set<onLineCallback>()
|
||||
export function addListener(cb: onLineCallback) {
|
||||
listeners.add(cb)
|
||||
return () => { listeners.delete(cb) }
|
||||
}
|
||||
|
||||
// Receive events from CommandLinePage and pass to listeners
|
||||
function handler(message: any) {
|
||||
if (message.command === "commandline") {
|
||||
for (let listener of listeners) {
|
||||
listener(message.exStr)
|
||||
}
|
||||
}
|
||||
// This is req. to shut typescript up.
|
||||
// TODO: Fix onMessageBool in web-ext-types
|
||||
return false
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(handler)
|
||||
}
|
||||
|
|
|
@ -9,15 +9,11 @@
|
|||
- see doc/escalating-privilege.md for other approaches.
|
||||
*/
|
||||
|
||||
namespace CommandLineContent {
|
||||
// inject the commandline iframe into a content page
|
||||
let clFrame = window.document.createElement("iframe")
|
||||
clFrame.setAttribute("src", browser.extension.getURL("static/commandline.html"))
|
||||
clFrame.setAttribute("style", "position: fixed; top: 0; left: 0; z-index: 10000; width: 100%; height: 36px; border: 0; padding: 0; margin: 0;");
|
||||
window.document.body.appendChild(clFrame)
|
||||
|
||||
// inject the commandline iframe into a content page
|
||||
let clFrame = window.document.createElement("iframe")
|
||||
clFrame.setAttribute("src", browser.extension.getURL("commandline/commandline.html"))
|
||||
clFrame.setAttribute("style", "position: fixed; top: 0; left: 0; z-index: 10000; width: 100%; height: 36px; border: 0; padding: 0; margin: 0;");
|
||||
window.document.body.appendChild(clFrame)
|
||||
|
||||
/** Focus the commandline input */
|
||||
export let focus = ():void => clFrame.focus()
|
||||
|
||||
}
|
||||
/** Focus the commandline input */
|
||||
export let focus = ():void => clFrame.focus()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/** Script used in the commandline iframe. Communicates with background. */
|
||||
|
||||
let clInput = window.document.getElementById("tridactyl-input") as HTMLInputElement
|
||||
clInput.focus()
|
||||
|
||||
|
@ -16,3 +17,6 @@ function process() {
|
|||
browser.runtime.sendMessage({command: "commandline", exStr: clInput.value})
|
||||
clInput.value = ""
|
||||
}
|
||||
|
||||
// Dummy export to ensure this is treated as a module
|
||||
export {}
|
10
src/content.ts
Normal file
10
src/content.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/** Content script entry point */
|
||||
|
||||
// Be careful: typescript elides imports that appear not to be used if they're
|
||||
// assigned to a name. If you want an import just for its side effects, make
|
||||
// sure you import it like this:
|
||||
import "./keydown_content"
|
||||
import "./commandline_content"
|
||||
import "./excmds_content"
|
||||
|
||||
console.log("Tridactyl content script loaded, boss!")
|
60
src/controller.ts
Normal file
60
src/controller.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import * as Parsing from "./parsing"
|
||||
|
||||
/** Accepts keyevents, resolves them to maps, maps to exstrs, executes exstrs */
|
||||
function *ParserController () {
|
||||
while (true) {
|
||||
let ex_str = ""
|
||||
let keys = []
|
||||
try {
|
||||
while (true) {
|
||||
let keyevent = yield
|
||||
let keypress = keyevent.key
|
||||
|
||||
// Special keys (e.g. Backspace) are not handled properly
|
||||
// yet. So drop them. This also drops all modifier keys.
|
||||
// When we put in handling for other special keys, remember
|
||||
// to continue to ban modifiers.
|
||||
if (keypress.length > 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
keys.push(keypress)
|
||||
let response = Parsing.normalmode.parser(keys)
|
||||
|
||||
console.debug(keys, response)
|
||||
|
||||
if (response.ex_str){
|
||||
ex_str = response.ex_str
|
||||
break
|
||||
} else {
|
||||
keys = response.keys
|
||||
}
|
||||
}
|
||||
acceptExCmd(ex_str)
|
||||
} catch (e) {
|
||||
// Rumsfeldian errors are caught here
|
||||
console.error("Tridactyl ParserController fatally wounded:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let generator = ParserController() // var rather than let stops weirdness in repl.
|
||||
generator.next()
|
||||
|
||||
/** Feed keys to the ParserController */
|
||||
export function acceptKey(keyevent: Event) {
|
||||
generator.next(keyevent)
|
||||
}
|
||||
|
||||
/** Parse and execute ExCmds */
|
||||
export function acceptExCmd(ex_str: string) {
|
||||
let [func, args] = Parsing.exmode.parser(ex_str)
|
||||
|
||||
try {
|
||||
func(...args)
|
||||
} catch (e) {
|
||||
// Errors from func are caught here (e.g. no next tab)
|
||||
// TODO: Errors should go to CommandLine.
|
||||
console.error(e)
|
||||
}
|
||||
}
|
|
@ -1,139 +1,122 @@
|
|||
/** Conventional definition of modulo that never gives a -ve result. */
|
||||
Number.prototype.mod = function (n: number): number {
|
||||
return Math.abs(this % n)
|
||||
}
|
||||
|
||||
// Implementation for all built-in ExCmds
|
||||
//
|
||||
// Example code. Needs to be replaced
|
||||
namespace ExCmds {
|
||||
|
||||
interface ContentCommandMessage {
|
||||
type: string
|
||||
command: string
|
||||
args?: Array<any>
|
||||
}
|
||||
|
||||
function messageActiveTab(command: string, args?: Array<any>) {
|
||||
messageFilteredTabs({active:true}, command, args)
|
||||
}
|
||||
|
||||
async function messageFilteredTabs(filter, command: string, args?: Array<any>) {
|
||||
let message: ContentCommandMessage = {type: "excmd_contentcommand", command: command}
|
||||
if (!(args == undefined)) message.args = args
|
||||
|
||||
let filtTabs = await browser.tabs.query(filter)
|
||||
filtTabs.map((tab) => {
|
||||
browser.tabs.sendMessage(tab.id,message)
|
||||
})
|
||||
}
|
||||
|
||||
// Scrolling functions
|
||||
export function scrolldown(n = 1) { messageActiveTab("scrollpx", [n]) }
|
||||
export function scrollup(n = 1) { scrolldown(n*-1) }
|
||||
|
||||
export function scrolldownpage(n = 1) { messageActiveTab("scrollpage", [n]) }
|
||||
export function scrolluppage(n = 1) { scrolldownpage(n*-1) }
|
||||
|
||||
export async function scrolldownhalfpage(n = 1) {
|
||||
const current_window = await browser.windows.getCurrent()
|
||||
scrolldown(n*0.5*current_window.height)
|
||||
}
|
||||
export function scrolluphalfpage(n = 1) { scrolldownhalfpage(n*-1) }
|
||||
|
||||
export function scrolldownline(n = 1) { messageActiveTab("scrollline", [n]) }
|
||||
export function scrollupline(n = 1) { scrolldownline(n*-1) }
|
||||
|
||||
|
||||
// Tab functions
|
||||
|
||||
// TODO: to be implemented!
|
||||
export async function getnexttabs(tabid: number, n: number){
|
||||
return [tabid]
|
||||
}
|
||||
|
||||
function tabSetActive(id: number) {
|
||||
browser.tabs.update(id,{active:true})
|
||||
}
|
||||
|
||||
export function closetabs(ids: number[]){
|
||||
browser.tabs.remove(ids)
|
||||
}
|
||||
|
||||
export async function getactivetabid(){
|
||||
return (await browser.tabs.query({active: true}))[0].id
|
||||
}
|
||||
|
||||
// NB: it is unclear how to undo tab closure.
|
||||
export async function tabclose(n = 1){
|
||||
let activeTabID = await getactivetabid()
|
||||
closetabs(await getnexttabs(activeTabID,n))
|
||||
}
|
||||
|
||||
export async function reload(n = 1, hard = false){
|
||||
let tabstoreload = await getnexttabs(await getactivetabid(),n)
|
||||
let reloadProperties = {bypassCache: hard}
|
||||
tabstoreload.map(
|
||||
(n)=>browser.tabs.reload(n, reloadProperties)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: address should default to some page to which we have access
|
||||
// and focus the location bar
|
||||
export async function tabopen(address?: string){
|
||||
browser.tabs.create({url: address})
|
||||
}
|
||||
|
||||
export async function reloadhard(n = 1){
|
||||
reload(n, true)
|
||||
}
|
||||
|
||||
/** Switch to the next tab by index (position on tab bar), wrapping round.
|
||||
|
||||
optional increment is number of tabs forwards to move.
|
||||
*/
|
||||
export async function tabnext(increment = 1) {
|
||||
try {
|
||||
// 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})
|
||||
|
||||
// Find the active tab (this is safe: there will only ever be one)
|
||||
let activeTab = tabs.filter((tab: any) => {return tab.active})[0]
|
||||
|
||||
// Derive the index we want
|
||||
let desiredIndex = (activeTab.index + increment).mod(tabs.length)
|
||||
|
||||
// Find and switch to the tab with that index
|
||||
let desiredTab = tabs.filter((tab: any) => {return tab.index === desiredIndex})[0]
|
||||
tabSetActive(desiredTab.id)
|
||||
}
|
||||
catch(error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
export function tabprev(increment = 1) { tabnext(increment*-1) }
|
||||
|
||||
// History functions
|
||||
export function history(n = 1) {messageActiveTab("history",[n])}
|
||||
export function historyback(n = 1) {history(n*-1)}
|
||||
export function historyforward(n = 1) {history(n)}
|
||||
|
||||
// Misc functions
|
||||
export function focuscmdline() { messageActiveTab("focuscmdline") }
|
||||
interface ContentCommandMessage {
|
||||
type: string
|
||||
command: string
|
||||
args?: Array<any>
|
||||
}
|
||||
|
||||
// From main.ts
|
||||
function messageActiveTab(command: string, args?: Array<any>) {
|
||||
messageFilteredTabs({active:true}, command, args)
|
||||
}
|
||||
|
||||
/* export async function incTab(increment: number) { */
|
||||
/* try { */
|
||||
/* let current_window = await browser.windows.getCurrent() */
|
||||
/* let tabs = await browser.tabs.query({windowId:current_window.id}) */
|
||||
/* let activeTab = tabs.filter((tab: any) => {return tab.active})[0] */
|
||||
/* let desiredIndex = (activeTab.index + increment).mod(tabs.length) */
|
||||
/* let desiredTab = tabs.filter((tab: any) => {return tab.index == desiredIndex})[0] */
|
||||
/* setTab(desiredTab.id) */
|
||||
/* } */
|
||||
/* catch(error) { */
|
||||
/* console.log(error) */
|
||||
/* } */
|
||||
async function messageFilteredTabs(filter, command: string, args?: Array<any>) {
|
||||
let message: ContentCommandMessage = {type: "excmd_contentcommand", command: command}
|
||||
if (!(args == undefined)) message.args = args
|
||||
|
||||
/* export async function setTab(id: number) { */
|
||||
/* browser.tabs.update(id,{active:true}) */
|
||||
/* } */
|
||||
let filtTabs = await browser.tabs.query(filter)
|
||||
filtTabs.map((tab) => {
|
||||
browser.tabs.sendMessage(tab.id,message)
|
||||
})
|
||||
}
|
||||
|
||||
// Scrolling functions
|
||||
export function scrolldown(n = 1) { messageActiveTab("scrollpx", [n]) }
|
||||
export function scrollup(n = 1) { scrolldown(n*-1) }
|
||||
|
||||
export function scrolldownpage(n = 1) { messageActiveTab("scrollpage", [n]) }
|
||||
export function scrolluppage(n = 1) { scrolldownpage(n*-1) }
|
||||
|
||||
export async function scrolldownhalfpage(n = 1) {
|
||||
const current_window = await browser.windows.getCurrent()
|
||||
scrolldown(n*0.5*current_window.height)
|
||||
}
|
||||
export function scrolluphalfpage(n = 1) { scrolldownhalfpage(n*-1) }
|
||||
|
||||
export function scrolldownline(n = 1) { messageActiveTab("scrollline", [n]) }
|
||||
export function scrollupline(n = 1) { scrolldownline(n*-1) }
|
||||
|
||||
|
||||
// Tab functions
|
||||
|
||||
// TODO: to be implemented!
|
||||
export async function getnexttabs(tabid: number, n: number){
|
||||
return [tabid]
|
||||
}
|
||||
|
||||
function tabSetActive(id: number) {
|
||||
browser.tabs.update(id,{active:true})
|
||||
}
|
||||
|
||||
export function closetabs(ids: number[]){
|
||||
browser.tabs.remove(ids)
|
||||
}
|
||||
|
||||
export async function getactivetabid(){
|
||||
return (await browser.tabs.query({active: true}))[0].id
|
||||
}
|
||||
|
||||
// NB: it is unclear how to undo tab closure.
|
||||
export async function tabclose(n = 1){
|
||||
let activeTabID = await getactivetabid()
|
||||
closetabs(await getnexttabs(activeTabID,n))
|
||||
}
|
||||
|
||||
export async function reload(n = 1, hard = false){
|
||||
let tabstoreload = await getnexttabs(await getactivetabid(),n)
|
||||
let reloadProperties = {bypassCache: hard}
|
||||
tabstoreload.map(
|
||||
(n)=>browser.tabs.reload(n, reloadProperties)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: address should default to some page to which we have access
|
||||
// and focus the location bar
|
||||
export async function tabopen(address?: string){
|
||||
browser.tabs.create({url: address})
|
||||
}
|
||||
|
||||
export async function reloadhard(n = 1){
|
||||
reload(n, true)
|
||||
}
|
||||
|
||||
/** Switch to the next tab by index (position on tab bar), wrapping round.
|
||||
|
||||
optional increment is number of tabs forwards to move.
|
||||
*/
|
||||
export async function tabnext(increment = 1) {
|
||||
try {
|
||||
// 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})
|
||||
|
||||
// Find the active tab (this is safe: there will only ever be one)
|
||||
let activeTab = tabs.filter((tab: any) => {return tab.active})[0]
|
||||
|
||||
// Derive the index we want
|
||||
let desiredIndex = (activeTab.index + increment).mod(tabs.length)
|
||||
|
||||
// Find and switch to the tab with that index
|
||||
let desiredTab = tabs.filter((tab: any) => {return tab.index === desiredIndex})[0]
|
||||
tabSetActive(desiredTab.id)
|
||||
}
|
||||
catch(error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
export function tabprev(increment = 1) { tabnext(increment*-1) }
|
||||
|
||||
// History functions
|
||||
export function history(n = 1) {messageActiveTab("history",[n])}
|
||||
export function historyback(n = 1) {history(n*-1)}
|
||||
export function historyforward(n = 1) {history(n)}
|
||||
|
||||
// Misc functions
|
||||
export function focuscmdline() { messageActiveTab("focuscmdline") }
|
||||
|
|
|
@ -1,92 +1,43 @@
|
|||
import * as CommandLineContent from './commandline_content'
|
||||
|
||||
// Some supporting stuff for ExCmds.
|
||||
// Example code. Needs to be replaced
|
||||
namespace ExCmdsContent {
|
||||
|
||||
type ContentCommand = (...any) => void
|
||||
type ContentCommand = (...any) => void
|
||||
|
||||
/** Functions to perform actions on the content page */
|
||||
// Could build these with a factory, but that breaks introspection because
|
||||
// .name is a read-only value.
|
||||
const commands = new Map<string, ContentCommand>([
|
||||
function scrollpx(n: number) {
|
||||
window.scrollBy(0, n)
|
||||
},
|
||||
function scrollline(n: number) {
|
||||
window.scrollByLines(n)
|
||||
},
|
||||
function scrollpage(n: number) {
|
||||
window.scrollByPages(n)
|
||||
},
|
||||
function history(n: number) {
|
||||
window.history.go(n)
|
||||
},
|
||||
function focuscmdline() {
|
||||
CommandLineContent.focus()
|
||||
}
|
||||
].map((command):any => [command.name, command]))
|
||||
/** Functions to perform actions on the content page */
|
||||
// Could build these with a factory, but that breaks introspection because
|
||||
// .name is a read-only value.
|
||||
const commands = new Map<string, ContentCommand>([
|
||||
function scrollpx(n: number) {
|
||||
window.scrollBy(0, n)
|
||||
},
|
||||
function scrollline(n: number) {
|
||||
window.scrollByLines(n)
|
||||
},
|
||||
function scrollpage(n: number) {
|
||||
window.scrollByPages(n)
|
||||
},
|
||||
function history(n: number) {
|
||||
window.history.go(n)
|
||||
},
|
||||
function focuscmdline() {
|
||||
CommandLineContent.focus()
|
||||
}
|
||||
].map((command):any => [command.name, command]))
|
||||
|
||||
function messageReceiver(message) {
|
||||
if (message.type === "excmd_contentcommand") {
|
||||
console.log(message)
|
||||
if (commands.has(message.command)) {
|
||||
if (message.args == null) {
|
||||
commands.get(message.command)()
|
||||
} else {
|
||||
commands.get(message.command)(...message.args)
|
||||
}
|
||||
function messageReceiver(message) {
|
||||
if (message.type === "excmd_contentcommand") {
|
||||
console.log(message)
|
||||
if (commands.has(message.command)) {
|
||||
if (message.args == null) {
|
||||
commands.get(message.command)()
|
||||
} else {
|
||||
console.error("Invalid excmd_contentcommand!", message)
|
||||
commands.get(message.command)(...message.args)
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid excmd_contentcommand!", message)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Tridactyl content script loaded, boss!")
|
||||
browser.runtime.onMessage.addListener(messageReceiver)
|
||||
|
||||
/* function historyHandler(message: Message) { */
|
||||
/* window.history.go(message.number) */
|
||||
/* } */
|
||||
|
||||
/* function scrollHandler(message: Message, scope?: string) { */
|
||||
/* if (!scope) window.scrollBy(0, message.number) */
|
||||
/* else if (scope === "lines") window.scrollByLines(message.number) */
|
||||
/* else if (scope === "pages") window.scrollByPages(message.number) */
|
||||
/* } */
|
||||
|
||||
/* function evalHandler(message: Message) { */
|
||||
/* eval(message.string) */
|
||||
/* } */
|
||||
|
||||
/* function messageHandler(message: Message): boolean { */
|
||||
/* switch(message.command) { */
|
||||
/* case "history": */
|
||||
/* historyHandler(message) */
|
||||
/* break */
|
||||
/* case "scroll": */
|
||||
/* scrollHandler(message) */
|
||||
/* break */
|
||||
/* case "scroll_lines": */
|
||||
/* scrollHandler(message, "lines") */
|
||||
/* break */
|
||||
/* case "scroll_pages": */
|
||||
/* scrollHandler(message, "pages") */
|
||||
/* break */
|
||||
/* case "focusCommandLine": */
|
||||
/* focusCommandLine() */
|
||||
/* break */
|
||||
/* case "eval": */
|
||||
/* evalHandler(message) */
|
||||
/* break */
|
||||
/* } */
|
||||
/* return true */
|
||||
/* } */
|
||||
|
||||
/* function sleep(ms: Number) { */
|
||||
/* return new Promise(function (resolve) { */
|
||||
/* setTimeout(resolve, ms) */
|
||||
/* }) */
|
||||
/* } */
|
||||
|
||||
/* console.log("Tridactyl content script loaded, boss!") */
|
||||
/* browser.runtime.onMessage.addListener(messageHandler) */
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(messageReceiver)
|
||||
|
|
|
@ -1,33 +1,31 @@
|
|||
namespace keydown_background {
|
||||
// Interface: onKeydown.addListener(func)
|
||||
// Interface: onKeydown.addListener(func)
|
||||
|
||||
// Type for messages sent from keydown_content
|
||||
interface KeydownShimMessage {
|
||||
command: string
|
||||
event: Event
|
||||
}
|
||||
|
||||
type KeydownCallback = (keyevent: Event) => void
|
||||
|
||||
const listeners = new Set<KeydownCallback>()
|
||||
function addListener(cb: KeydownCallback) {
|
||||
listeners.add(cb)
|
||||
return () => { listeners.delete(cb) }
|
||||
}
|
||||
|
||||
export const onKeydown = { addListener }
|
||||
|
||||
// Receive events from content and pass to listeners
|
||||
function handler(message: KeydownShimMessage) {
|
||||
if (message.command === "keydown") {
|
||||
for (let listener of listeners) {
|
||||
listener(message.event)
|
||||
}
|
||||
}
|
||||
// This is req. to shut typescript up.
|
||||
// TODO: Fix onMessageBool in web-ext-types
|
||||
return false
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(handler)
|
||||
// Type for messages sent from keydown_content
|
||||
interface KeydownShimMessage {
|
||||
command: string
|
||||
event: Event
|
||||
}
|
||||
|
||||
type KeydownCallback = (keyevent: Event) => void
|
||||
|
||||
const listeners = new Set<KeydownCallback>()
|
||||
function addListener(cb: KeydownCallback) {
|
||||
listeners.add(cb)
|
||||
return () => { listeners.delete(cb) }
|
||||
}
|
||||
|
||||
export const onKeydown = { addListener }
|
||||
|
||||
// Receive events from content and pass to listeners
|
||||
function handler(message: KeydownShimMessage) {
|
||||
if (message.command === "keydown") {
|
||||
for (let listener of listeners) {
|
||||
listener(message.event)
|
||||
}
|
||||
}
|
||||
// This is req. to shut typescript up.
|
||||
// TODO: Fix onMessageBool in web-ext-types
|
||||
return false
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(handler)
|
||||
|
|
|
@ -1,90 +1,54 @@
|
|||
/* Shim for the keyboard API because it won't hit in FF57. */
|
||||
/** Shim for the keyboard API because it won't hit in FF57. */
|
||||
|
||||
/* Interface:
|
||||
|
||||
All keyboard events will be sent to whatever listens to Port keydown-shim.
|
||||
// {{{ Helper functions
|
||||
|
||||
oldUse:
|
||||
|
||||
function setup_port(p) {
|
||||
if (p.name === "keydown-shim") {
|
||||
this.port = p
|
||||
this.unlisten()
|
||||
}
|
||||
}
|
||||
|
||||
export var port: browser.runtime.Port = undefined
|
||||
var unlisten = browser.runtime.onConnect.addListener(setup_port)
|
||||
|
||||
newUse:
|
||||
|
||||
function listen(msg) {
|
||||
if (msg.name === "keydown") {
|
||||
feed(msg.keydown)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
...
|
||||
browser.tabs.sendMessage({command: "keydown-suppress", preventDefault: true})
|
||||
...
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(listen);
|
||||
|
||||
*/
|
||||
|
||||
namespace keydown_content {
|
||||
// {{{ Helper functions
|
||||
|
||||
function pick (o, ...props) {
|
||||
return Object.assign({}, ...props.map(prop => ({[prop]: o[prop]})))
|
||||
}
|
||||
|
||||
// Shallow copy of keyevent.
|
||||
function shallowKeyboardEvent (ke): Event {
|
||||
let shallow = pick(
|
||||
ke,
|
||||
'shiftKey', 'metaKey', 'altKey', 'ctrlKey', 'repeat', 'key',
|
||||
'bubbles', 'composed', 'defaultPrevented', 'eventPhase',
|
||||
'timeStamp', 'type', 'isTrusted'
|
||||
)
|
||||
shallow.target = pick(ke.target, 'tagName')
|
||||
shallow.target.ownerDocument = pick(ke.target.ownerDocument, 'URL')
|
||||
return shallow
|
||||
} // }}}
|
||||
|
||||
function keyeventHandler(ke: KeyboardEvent) {
|
||||
// Suppress events, if requested
|
||||
if (preventDefault) {
|
||||
ke.preventDefault()
|
||||
}
|
||||
if (stopPropagation) {
|
||||
ke.stopPropagation()
|
||||
}
|
||||
browser.runtime.sendMessage({command: "keydown", event: shallowKeyboardEvent(ke)})
|
||||
}
|
||||
|
||||
// Listen for suppression messages from bg script.
|
||||
function backgroundListener(message) {
|
||||
if (message.command === "keydown-suppress") {
|
||||
if ('preventDefault' in message.data) {
|
||||
preventDefault = message.data.preventDefault
|
||||
}
|
||||
if ('stopPropagation' in message.data) {
|
||||
stopPropagation = message.data.stopPropagation
|
||||
}
|
||||
}
|
||||
// This is to shut up the type checker.
|
||||
return false
|
||||
}
|
||||
|
||||
// State
|
||||
let preventDefault = false
|
||||
let stopPropagation = false
|
||||
|
||||
// Add listeners
|
||||
window.addEventListener("keydown", keyeventHandler)
|
||||
browser.runtime.onMessage.addListener(backgroundListener)
|
||||
function pick (o, ...props) {
|
||||
return Object.assign({}, ...props.map(prop => ({[prop]: o[prop]})))
|
||||
}
|
||||
|
||||
// Shallow copy of keyevent.
|
||||
function shallowKeyboardEvent (ke): Event {
|
||||
let shallow = pick(
|
||||
ke,
|
||||
'shiftKey', 'metaKey', 'altKey', 'ctrlKey', 'repeat', 'key',
|
||||
'bubbles', 'composed', 'defaultPrevented', 'eventPhase',
|
||||
'timeStamp', 'type', 'isTrusted'
|
||||
)
|
||||
shallow.target = pick(ke.target, 'tagName')
|
||||
shallow.target.ownerDocument = pick(ke.target.ownerDocument, 'URL')
|
||||
return shallow
|
||||
} // }}}
|
||||
|
||||
function keyeventHandler(ke: KeyboardEvent) {
|
||||
// Suppress events, if requested
|
||||
if (preventDefault) {
|
||||
ke.preventDefault()
|
||||
}
|
||||
if (stopPropagation) {
|
||||
ke.stopPropagation()
|
||||
}
|
||||
browser.runtime.sendMessage({command: "keydown", event: shallowKeyboardEvent(ke)})
|
||||
}
|
||||
|
||||
// Listen for suppression messages from bg script.
|
||||
function backgroundListener(message) {
|
||||
if (message.command === "keydown-suppress") {
|
||||
if ('preventDefault' in message.data) {
|
||||
preventDefault = message.data.preventDefault
|
||||
}
|
||||
if ('stopPropagation' in message.data) {
|
||||
stopPropagation = message.data.stopPropagation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// State
|
||||
let preventDefault = false
|
||||
let stopPropagation = false
|
||||
|
||||
// Add listeners
|
||||
window.addEventListener("keydown", keyeventHandler)
|
||||
browser.runtime.onMessage.addListener(backgroundListener)
|
||||
|
||||
// Dummy export so that TS treats this as a module.
|
||||
export {}
|
||||
|
|
82
src/main.ts
82
src/main.ts
|
@ -1,82 +0,0 @@
|
|||
interface Number {
|
||||
mod(n: number): number
|
||||
}
|
||||
|
||||
/** Conventional definition of modulo that never gives a -ve result. */
|
||||
Number.prototype.mod = function (n: number): number {
|
||||
return Math.abs(this % n)
|
||||
}
|
||||
|
||||
namespace Controller {
|
||||
|
||||
/** Accepts keyevents, resolves them to maps, maps to exstrs, executes exstrs */
|
||||
function *ParserController () {
|
||||
while (true) {
|
||||
let ex_str = ""
|
||||
let keys = []
|
||||
try {
|
||||
while (true) {
|
||||
let keyevent = yield
|
||||
let keypress = keyevent.key
|
||||
|
||||
// Special keys (e.g. Backspace) are not handled properly
|
||||
// yet. So drop them. This also drops all modifier keys.
|
||||
// When we put in handling for other special keys, remember
|
||||
// to continue to ban modifiers.
|
||||
if (keypress.length > 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
keys.push(keypress)
|
||||
let response = Parsing.normalmode.parser(keys)
|
||||
|
||||
console.debug(keys, response)
|
||||
|
||||
if (response.ex_str){
|
||||
ex_str = response.ex_str
|
||||
break
|
||||
} else {
|
||||
keys = response.keys
|
||||
}
|
||||
}
|
||||
acceptExCmd(ex_str)
|
||||
} catch (e) {
|
||||
// Rumsfeldian errors are caught here
|
||||
console.error("Tridactyl ParserController fatally wounded:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let generator = ParserController() // var rather than let stops weirdness in repl.
|
||||
generator.next()
|
||||
|
||||
/** Feed keys to the ParserController */
|
||||
export function acceptKey(keyevent: Event) {
|
||||
generator.next(keyevent)
|
||||
}
|
||||
|
||||
/** Parse and execute ExCmds */
|
||||
export function acceptExCmd(ex_str: string) {
|
||||
let [func, args] = Parsing.exmode.parser(ex_str)
|
||||
|
||||
try {
|
||||
func(...args)
|
||||
} catch (e) {
|
||||
// Errors from func are caught here (e.g. no next tab)
|
||||
// TODO: Errors should go to CommandLine.
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
export function init() {
|
||||
// Send keys to controller
|
||||
keydown_background.onKeydown.addListener(acceptKey)
|
||||
// To eventually be replaced by:
|
||||
// browser.keyboard.onKeydown.addListener
|
||||
|
||||
// Send commandline to controller
|
||||
CommandLine.onLine.addListener(acceptExCmd)
|
||||
}
|
||||
}
|
||||
|
||||
Controller.init()
|
|
@ -5,18 +5,18 @@
|
|||
"version": "1.0",
|
||||
|
||||
"background" : {
|
||||
"scripts": ["keydown_background.js", "commandline_background.js", "excmds_background.js", "parsing.js", "main.js"]
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches":["<all_urls>"],
|
||||
"js":["keydown_content.js", "commandline_content.js", "excmds_content.js"]
|
||||
"js":["content.js"]
|
||||
}
|
||||
],
|
||||
|
||||
"web_accessible_resources": [
|
||||
"commandline/commandline.html"
|
||||
"static/commandline.html"
|
||||
],
|
||||
|
||||
|
||||
|
|
18
src/package-lock.json
generated
18
src/package-lock.json
generated
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "tridactyl",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"typescript": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz",
|
||||
"integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==",
|
||||
"dev": true
|
||||
},
|
||||
"web-ext-types": {
|
||||
"version": "github:michael-zapata/web-ext-types#7b8cae3212a71f41224f4bbf1cb00b213fb5d10e",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,7 @@
|
|||
"description": "Vimperator/Pentadactyl successor",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"typescript": "^2.5.3",
|
||||
"web-ext-types": "github:michael-zapata/web-ext-types"
|
||||
"web-ext-types": "0.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./make"
|
||||
|
|
208
src/parsing.ts
208
src/parsing.ts
|
@ -1,118 +1,118 @@
|
|||
import * as ExCmds from "./excmds_background"
|
||||
|
||||
// Simple implementations of a normal and ex mode parsers
|
||||
namespace Parsing {
|
||||
|
||||
// Tridactyl normal mode:
|
||||
//
|
||||
// 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.
|
||||
export namespace normalmode {
|
||||
// Tridactyl normal mode:
|
||||
//
|
||||
// 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.
|
||||
export namespace normalmode {
|
||||
|
||||
// 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
|
||||
const nmaps = new Map<string, string>([
|
||||
["t", "tabopen"],
|
||||
["j", "scrolldownline"],
|
||||
["H", "historyback"],
|
||||
["L", "historyforward"],
|
||||
["d", "tabclose"],
|
||||
["r", "reload"],
|
||||
["R", "reloadhard"],
|
||||
["k", "scrollupline"],
|
||||
["gt", "tabnext"],
|
||||
["gT", "tabprev"],
|
||||
["gr", "reader"],
|
||||
[":", "focuscmdline"],
|
||||
["s", "open google"],
|
||||
["xx", "something"],
|
||||
// Special keys must be prepended with 🄰
|
||||
// ["🄰Backspace", "something"],
|
||||
])
|
||||
// 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
|
||||
const nmaps = new Map<string, string>([
|
||||
["t", "tabopen"],
|
||||
["j", "scrolldownline"],
|
||||
["H", "historyback"],
|
||||
["L", "historyforward"],
|
||||
["d", "tabclose"],
|
||||
["r", "reload"],
|
||||
["R", "reloadhard"],
|
||||
["k", "scrollupline"],
|
||||
["gt", "tabnext"],
|
||||
["gT", "tabprev"],
|
||||
["gr", "reader"],
|
||||
[":", "focuscmdline"],
|
||||
["s", "open google"],
|
||||
["xx", "something"],
|
||||
// Special keys must be prepended with 🄰
|
||||
// ["🄰Backspace", "something"],
|
||||
])
|
||||
|
||||
// Split a string into a number prefix and some following keys.
|
||||
function keys_split_count(keys: string[]){
|
||||
// Extracts the first number with capturing parentheses
|
||||
const FIRST_NUM_REGEX = /^([0-9]+)/
|
||||
// Split a string into a number prefix and some following keys.
|
||||
function keys_split_count(keys: string[]){
|
||||
// Extracts the first number with capturing parentheses
|
||||
const FIRST_NUM_REGEX = /^([0-9]+)/
|
||||
|
||||
let keystr = keys.join("")
|
||||
let regexCapture = FIRST_NUM_REGEX.exec(keystr)
|
||||
let count = regexCapture ? regexCapture[0] : null
|
||||
keystr = keystr.replace(FIRST_NUM_REGEX,"")
|
||||
return [count, keystr]
|
||||
let keystr = keys.join("")
|
||||
let regexCapture = FIRST_NUM_REGEX.exec(keystr)
|
||||
let count = regexCapture ? regexCapture[0] : null
|
||||
keystr = keystr.replace(FIRST_NUM_REGEX,"")
|
||||
return [count, keystr]
|
||||
}
|
||||
|
||||
// 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.get(map)
|
||||
}
|
||||
|
||||
// Valid keystr to ex_str by splitting count, resolving keystr and appending count as final argument.
|
||||
// TODO: This is a naive way to deal with counts and won't work for ExCmds that don't expect a numeric answer.
|
||||
// TODO: Refactor to return a ExCmdPartial object?
|
||||
function get_ex_str(keys): string {
|
||||
let [count, keystr] = keys_split_count(keys)
|
||||
let ex_str = resolve_map(keystr)
|
||||
if (ex_str){
|
||||
ex_str = count ? ex_str + " " + count : ex_str
|
||||
}
|
||||
return ex_str
|
||||
}
|
||||
|
||||
// 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.get(map)
|
||||
}
|
||||
|
||||
// Valid keystr to ex_str by splitting count, resolving keystr and appending count as final argument.
|
||||
// TODO: This is a naive way to deal with counts and won't work for ExCmds that don't expect a numeric answer.
|
||||
// TODO: Refactor to return a ExCmdPartial object?
|
||||
function get_ex_str(keys): string {
|
||||
let [count, keystr] = keys_split_count(keys)
|
||||
let ex_str = resolve_map(keystr)
|
||||
if (ex_str){
|
||||
ex_str = count ? ex_str + " " + count : ex_str
|
||||
}
|
||||
return ex_str
|
||||
}
|
||||
|
||||
// A list of maps that keys could potentially map to.
|
||||
function possible_maps(keys): string[] {
|
||||
let [count, keystr] = keys_split_count(keys)
|
||||
|
||||
// Short circuit or search maps.
|
||||
if (nmaps.has(keystr)) {
|
||||
return [keystr,]
|
||||
} else {
|
||||
// Efficiency: this can be short-circuited.
|
||||
return completions(keystr)
|
||||
}
|
||||
}
|
||||
|
||||
// A list of maps that start with the fragment.
|
||||
export function completions(fragment): string[] {
|
||||
let posskeystrs = Array.from(nmaps.keys())
|
||||
return posskeystrs.filter((key)=>key.startsWith(fragment))
|
||||
}
|
||||
|
||||
interface NormalResponse {
|
||||
keys?: string[]
|
||||
ex_str?: string
|
||||
}
|
||||
|
||||
export function parser(keys): NormalResponse {
|
||||
// If there aren't any possible matches, throw away keys until there are
|
||||
while ((possible_maps(keys).length == 0) && (keys.length)) {
|
||||
keys = keys.slice(1)
|
||||
}
|
||||
|
||||
// If keys map to an ex_str, send it
|
||||
let ex_str = get_ex_str(keys)
|
||||
if (ex_str){
|
||||
return {ex_str}
|
||||
}
|
||||
// Otherwise, return the keys that might be used in a future command
|
||||
return {keys}
|
||||
// A list of maps that keys could potentially map to.
|
||||
function possible_maps(keys): string[] {
|
||||
let [count, keystr] = keys_split_count(keys)
|
||||
|
||||
// Short circuit or search maps.
|
||||
if (nmaps.has(keystr)) {
|
||||
return [keystr,]
|
||||
} else {
|
||||
// Efficiency: this can be short-circuited.
|
||||
return completions(keystr)
|
||||
}
|
||||
}
|
||||
|
||||
// Ex Mode (AKA cmd mode)
|
||||
export namespace exmode {
|
||||
// A list of maps that start with the fragment.
|
||||
export function completions(fragment): string[] {
|
||||
let posskeystrs = Array.from(nmaps.keys())
|
||||
return posskeystrs.filter((key)=>key.startsWith(fragment))
|
||||
}
|
||||
|
||||
// Simplistic Ex command line parser.
|
||||
// TODO: Quoting arguments
|
||||
// TODO: Pipe to separate commands
|
||||
export function parser(ex_str){
|
||||
let [func,...args] = ex_str.split(" ")
|
||||
if (func in ExCmds) {
|
||||
return [ExCmds[func], args]
|
||||
} else {
|
||||
throw `Not an excmd: ${func}`
|
||||
}
|
||||
interface NormalResponse {
|
||||
keys?: string[]
|
||||
ex_str?: string
|
||||
}
|
||||
|
||||
export function parser(keys): NormalResponse {
|
||||
// If there aren't any possible matches, throw away keys until there are
|
||||
while ((possible_maps(keys).length == 0) && (keys.length)) {
|
||||
keys = keys.slice(1)
|
||||
}
|
||||
|
||||
// If keys map to an ex_str, send it
|
||||
let ex_str = get_ex_str(keys)
|
||||
if (ex_str){
|
||||
return {ex_str}
|
||||
}
|
||||
// Otherwise, return the keys that might be used in a future command
|
||||
return {keys}
|
||||
}
|
||||
}
|
||||
|
||||
// Ex Mode (AKA cmd mode)
|
||||
export namespace exmode {
|
||||
|
||||
// Simplistic Ex command line parser.
|
||||
// TODO: Quoting arguments
|
||||
// TODO: Pipe to separate commands
|
||||
export function parser(ex_str){
|
||||
let [func,...args] = ex_str.split(" ")
|
||||
if (func in ExCmds) {
|
||||
return [ExCmds[func], args]
|
||||
} else {
|
||||
throw `Not an excmd: ${func}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
</head>
|
||||
<body>
|
||||
<input id=tridactyl-input></input>
|
||||
<script src="commandline.js"></script>
|
||||
<script src="../commandline_frame.js"></script>
|
||||
</body>
|
||||
</html>
|
4
src/tridactyl.d.ts
vendored
4
src/tridactyl.d.ts
vendored
|
@ -3,6 +3,10 @@
|
|||
// For some obscure reason, tsc doesn't like .d.ts files to share a name with
|
||||
// .ts files. So don't do that.
|
||||
|
||||
interface Number {
|
||||
mod(n: number): number
|
||||
}
|
||||
|
||||
// For content.ts
|
||||
interface Message {
|
||||
command?: string
|
||||
|
|
|
@ -2,9 +2,14 @@
|
|||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"noEmitOnError": true,
|
||||
"outDir": "../build",
|
||||
// outDir not used by webpack, just for debugging.
|
||||
"outDir": "build/tsc-out",
|
||||
"sourceMap": true,
|
||||
"target": "ES2017",
|
||||
"lib": ["ES2017","dom"],
|
||||
"typeRoots": ["@types", "node_modules/web-ext-types/"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
44
webpack.config.js
Normal file
44
webpack.config.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
|
||||
const CopyWebPackPlugin = require('copy-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
background: "./src/background.ts",
|
||||
content: "./src/content.ts",
|
||||
commandline_frame: "./src/commandline_frame.ts",
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: __dirname + "/build"
|
||||
},
|
||||
|
||||
// Enable sourcemaps for debugging webpack's output.
|
||||
devtool: "source-map",
|
||||
|
||||
resolve: {
|
||||
// Add '.ts' and '.tsx' as resolvable extensions.
|
||||
extensions: [".ts", ".tsx", ".js", ".json"]
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
|
||||
{ test: /\.tsx?$/, loader: "awesome-typescript-loader" },
|
||||
|
||||
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
|
||||
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// new UglifyJSPlugin({
|
||||
// uglifyOptions: {
|
||||
// ecma: 8
|
||||
// }
|
||||
// }),
|
||||
new CopyWebPackPlugin([
|
||||
{ from: "src/manifest.json" },
|
||||
{ from: "src/static", to: "static" },
|
||||
]),
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue