Merge branch 'feature-webpack-build'

This commit is contained in:
Colin Caine 2017-10-02 01:27:02 +01:00
commit cd9ba01d79
23 changed files with 4320 additions and 576 deletions

2
.gitignore vendored
View file

@ -1,2 +1,2 @@
build
src/node_modules
node_modules

3793
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
package.json Normal file
View 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
View 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)

View file

@ -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)
}

View file

@ -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()

View file

@ -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
View 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
View 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)
}
}

View file

@ -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") }

View file

@ -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)

View file

@ -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)

View file

@ -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 {}

View file

@ -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()

View file

@ -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
View file

@ -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
}
}
}

View file

@ -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"

View file

@ -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}`
}
}
}

View file

@ -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
View file

@ -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

View file

@ -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
View 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" },
]),
]
}