2017-09-29 18:29:36 +01:00
|
|
|
/** Script used in the commandline iframe. Communicates with background. */
|
2017-10-02 00:59:51 +01:00
|
|
|
|
2017-11-15 13:41:04 -08:00
|
|
|
import "./lib/html-tagged-template"
|
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
import * as Completions from "./completions"
|
|
|
|
import * as Messaging from "./messaging"
|
|
|
|
import * as Config from "./config"
|
|
|
|
import * as SELF from "./commandline_frame"
|
|
|
|
import "./number.clamp"
|
|
|
|
import state from "./state"
|
|
|
|
import Logger from "./logging"
|
|
|
|
import * as aliases from "./aliases"
|
2018-05-20 13:17:28 +01:00
|
|
|
import { theme } from "./styling"
|
2018-04-13 19:28:03 +01:00
|
|
|
const logger = new Logger("cmdline")
|
2017-10-05 23:11:56 +01:00
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
let activeCompletions: Completions.CompletionSource[] = undefined
|
2018-04-13 19:28:03 +01:00
|
|
|
let completionsDiv = window.document.getElementById(
|
|
|
|
"completions",
|
|
|
|
) as HTMLElement
|
|
|
|
let clInput = window.document.getElementById(
|
|
|
|
"tridactyl-input",
|
|
|
|
) as HTMLInputElement
|
2017-10-12 04:02:01 +01:00
|
|
|
|
2018-05-20 13:17:28 +01:00
|
|
|
// first theming of commandline iframe
|
|
|
|
theme(document.querySelector(":root"))
|
|
|
|
|
2017-11-15 13:41:04 -08:00
|
|
|
/* This is to handle Escape key which, while the cmdline is focused,
|
|
|
|
* ends up firing both keydown and input listeners. In the worst case
|
|
|
|
* hides the cmdline, shows and refocuses it and replaces its text
|
|
|
|
* which could be the prefix to generate a completion.
|
|
|
|
* tl;dr TODO: delete this and better resolve race condition
|
|
|
|
*/
|
|
|
|
let isVisible = false
|
2017-11-22 18:05:54 +00:00
|
|
|
function resizeArea() {
|
|
|
|
if (isVisible) {
|
|
|
|
Messaging.message("commandline_background", "show")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
// This is a bit loosely defined at the moment.
|
|
|
|
// Should work so long as there's only one completion source per prefix.
|
|
|
|
function getCompletion() {
|
|
|
|
for (const comp of activeCompletions) {
|
2018-04-13 19:28:03 +01:00
|
|
|
if (comp.state === "normal" && comp.completion !== undefined) {
|
2017-11-22 18:13:31 +00:00
|
|
|
return comp.completion
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function enableCompletions() {
|
2018-04-13 19:28:03 +01:00
|
|
|
if (!activeCompletions) {
|
2017-11-22 18:05:54 +00:00
|
|
|
activeCompletions = [
|
2018-06-17 06:21:10 +02:00
|
|
|
new Completions.BufferAllCompletionSource(completionsDiv),
|
2017-11-22 18:05:54 +00:00
|
|
|
new Completions.BufferCompletionSource(completionsDiv),
|
2017-11-23 14:20:44 +00:00
|
|
|
new Completions.HistoryCompletionSource(completionsDiv),
|
2017-11-30 17:02:31 +00:00
|
|
|
new Completions.BmarkCompletionSource(completionsDiv),
|
2017-11-22 18:05:54 +00:00
|
|
|
]
|
2017-11-15 13:41:04 -08:00
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
const fragment = document.createDocumentFragment()
|
|
|
|
activeCompletions.forEach(comp => fragment.appendChild(comp.node))
|
|
|
|
completionsDiv.appendChild(fragment)
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
|
|
|
}
|
2017-11-22 18:13:31 +00:00
|
|
|
/* document.addEventListener("DOMContentLoaded", enableCompletions) */
|
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
let noblur = e => setTimeout(() => clInput.focus(), 0)
|
2017-11-25 23:15:45 +00:00
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
export function focus() {
|
|
|
|
enableCompletions()
|
2018-04-13 19:28:03 +01:00
|
|
|
document.body.classList.remove("hidden")
|
2017-11-22 18:13:31 +00:00
|
|
|
clInput.focus()
|
2018-04-13 19:28:03 +01:00
|
|
|
clInput.addEventListener("blur", noblur)
|
2017-11-22 18:13:31 +00:00
|
|
|
}
|
2017-09-29 18:29:36 +01:00
|
|
|
|
2017-10-28 05:11:10 +01:00
|
|
|
async function sendExstr(exstr) {
|
|
|
|
Messaging.message("commandline_background", "recvExStr", [exstr])
|
|
|
|
}
|
|
|
|
|
2017-11-26 14:06:13 +00:00
|
|
|
let HISTORY_SEARCH_STRING: string
|
|
|
|
|
|
|
|
/* Command line keybindings */
|
2017-11-25 23:15:45 +00:00
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
clInput.addEventListener("keydown", function(keyevent) {
|
2017-11-05 14:48:22 +00:00
|
|
|
switch (keyevent.key) {
|
|
|
|
case "Enter":
|
|
|
|
process()
|
|
|
|
break
|
|
|
|
|
2017-11-25 15:12:40 +00:00
|
|
|
case "j":
|
2018-04-13 19:28:03 +01:00
|
|
|
if (keyevent.ctrlKey) {
|
2017-11-25 15:16:08 +00:00
|
|
|
// stop Firefox from giving focus to the omnibar
|
|
|
|
keyevent.preventDefault()
|
|
|
|
keyevent.stopPropagation()
|
|
|
|
process()
|
2017-11-25 15:12:40 +00:00
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case "m":
|
2018-04-13 19:28:03 +01:00
|
|
|
if (keyevent.ctrlKey) {
|
2017-11-25 15:16:08 +00:00
|
|
|
process()
|
2017-11-25 15:12:40 +00:00
|
|
|
}
|
|
|
|
break
|
|
|
|
|
2017-11-05 14:48:22 +00:00
|
|
|
case "Escape":
|
2017-11-22 23:01:34 +00:00
|
|
|
keyevent.preventDefault()
|
2017-11-05 14:48:22 +00:00
|
|
|
hide_and_clear()
|
|
|
|
break
|
|
|
|
|
|
|
|
// Todo: fish-style history search
|
|
|
|
// persistent history
|
|
|
|
case "ArrowUp":
|
|
|
|
history(-1)
|
|
|
|
break
|
|
|
|
|
|
|
|
case "ArrowDown":
|
|
|
|
history(1)
|
|
|
|
break
|
|
|
|
|
2018-01-05 21:36:12 -08:00
|
|
|
case "a":
|
|
|
|
if (keyevent.ctrlKey) {
|
|
|
|
keyevent.preventDefault()
|
|
|
|
keyevent.stopPropagation()
|
|
|
|
setCursor()
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case "e":
|
2018-04-13 19:28:03 +01:00
|
|
|
if (keyevent.ctrlKey) {
|
2018-01-05 21:36:12 -08:00
|
|
|
keyevent.preventDefault()
|
|
|
|
keyevent.stopPropagation()
|
|
|
|
setCursor(clInput.value.length)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case "u":
|
2018-04-13 19:28:03 +01:00
|
|
|
if (keyevent.ctrlKey) {
|
2018-01-05 21:36:12 -08:00
|
|
|
keyevent.preventDefault()
|
|
|
|
keyevent.stopPropagation()
|
2018-04-13 19:28:03 +01:00
|
|
|
clInput.value = clInput.value.slice(
|
|
|
|
clInput.selectionStart,
|
|
|
|
clInput.value.length,
|
|
|
|
)
|
2018-01-05 21:36:12 -08:00
|
|
|
setCursor()
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case "k":
|
2018-04-13 19:28:03 +01:00
|
|
|
if (keyevent.ctrlKey) {
|
2018-01-05 21:36:12 -08:00
|
|
|
keyevent.preventDefault()
|
|
|
|
keyevent.stopPropagation()
|
|
|
|
clInput.value = clInput.value.slice(0, clInput.selectionStart)
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
// Clear input on ^C if there is no selection
|
2017-11-05 14:48:22 +00:00
|
|
|
// Todo: hard mode: vi style editing on cli, like set -o mode vi
|
|
|
|
// should probably just defer to another library
|
|
|
|
case "c":
|
2018-04-13 19:28:03 +01:00
|
|
|
if (
|
|
|
|
keyevent.ctrlKey &&
|
|
|
|
!clInput.value.substring(
|
|
|
|
clInput.selectionStart,
|
|
|
|
clInput.selectionEnd,
|
|
|
|
)
|
|
|
|
) {
|
2018-01-05 21:36:12 -08:00
|
|
|
hide_and_clear()
|
|
|
|
}
|
2017-11-09 12:44:57 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
case "f":
|
2018-04-13 19:28:03 +01:00
|
|
|
if (keyevent.ctrlKey) {
|
2017-11-09 12:44:57 +00:00
|
|
|
// Stop ctrl+f from doing find
|
|
|
|
keyevent.preventDefault()
|
|
|
|
keyevent.stopPropagation()
|
|
|
|
tabcomplete()
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case "Tab":
|
|
|
|
// Stop tab from losing focus
|
|
|
|
keyevent.preventDefault()
|
|
|
|
keyevent.stopPropagation()
|
2018-04-13 19:28:03 +01:00
|
|
|
if (keyevent.shiftKey) {
|
|
|
|
activeCompletions.forEach(comp => comp.prev())
|
2017-11-24 19:00:26 +00:00
|
|
|
} else {
|
2018-04-13 19:28:03 +01:00
|
|
|
activeCompletions.forEach(comp => comp.next())
|
2017-11-24 19:00:26 +00:00
|
|
|
}
|
2017-11-23 15:44:07 +00:00
|
|
|
// tabcomplete()
|
2017-11-09 12:44:57 +00:00
|
|
|
break
|
2018-03-13 08:04:19 +01:00
|
|
|
|
|
|
|
case " ":
|
|
|
|
const command = getCompletion()
|
2018-04-27 16:45:50 +01:00
|
|
|
activeCompletions.forEach(comp => (comp.completion = undefined))
|
|
|
|
if (command) fillcmdline(command, false)
|
2018-03-13 08:04:19 +01:00
|
|
|
break
|
2017-10-09 12:40:23 -07:00
|
|
|
}
|
2017-11-26 14:06:13 +00:00
|
|
|
|
|
|
|
// If a key other than the arrow keys was pressed, clear the history search string
|
2018-04-13 19:28:03 +01:00
|
|
|
if (!(keyevent.key == "ArrowUp" || keyevent.key == "ArrowDown")) {
|
2017-11-26 14:06:13 +00:00
|
|
|
HISTORY_SEARCH_STRING = undefined
|
|
|
|
}
|
2017-09-29 18:29:36 +01:00
|
|
|
})
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
clInput.addEventListener("input", () => {
|
2017-12-31 16:00:00 +08:00
|
|
|
const exstr = clInput.value
|
2018-01-09 17:33:52 +08:00
|
|
|
const expandedCmd = aliases.expandExstr(exstr)
|
2017-12-28 17:58:52 +08:00
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
// Fire each completion and add a callback to resize area
|
2017-12-30 00:46:26 +00:00
|
|
|
logger.debug(activeCompletions)
|
2017-11-22 18:05:54 +00:00
|
|
|
activeCompletions.forEach(comp =>
|
2018-04-13 19:28:03 +01:00
|
|
|
comp.filter(expandedCmd).then(() => resizeArea()),
|
2017-11-22 18:05:54 +00:00
|
|
|
)
|
2017-11-15 13:41:04 -08:00
|
|
|
})
|
|
|
|
|
2017-11-05 14:48:22 +00:00
|
|
|
let cmdline_history_position = 0
|
|
|
|
let cmdline_history_current = ""
|
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
async function hide_and_clear() {
|
|
|
|
clInput.removeEventListener("blur", noblur)
|
2017-11-22 23:01:34 +00:00
|
|
|
clInput.value = ""
|
2017-11-26 14:06:13 +00:00
|
|
|
cmdline_history_position = 0
|
|
|
|
cmdline_history_current = ""
|
2017-11-22 18:13:31 +00:00
|
|
|
|
|
|
|
// Try to make the close cmdline animation as smooth as possible.
|
2018-04-13 19:28:03 +01:00
|
|
|
document.body.classList.add("hidden")
|
|
|
|
Messaging.message("commandline_background", "hide")
|
2017-11-22 18:05:54 +00:00
|
|
|
// Delete all completion sources - I don't think this is required, but this
|
|
|
|
// way if there is a transient bug in completions it shouldn't persist.
|
|
|
|
activeCompletions.forEach(comp => completionsDiv.removeChild(comp.node))
|
|
|
|
activeCompletions = undefined
|
2017-11-15 13:41:04 -08:00
|
|
|
isVisible = false
|
2017-11-05 14:48:22 +00:00
|
|
|
}
|
|
|
|
|
2018-01-05 21:36:12 -08:00
|
|
|
function setCursor(n = 0) {
|
|
|
|
clInput.setSelectionRange(n, n, "none")
|
|
|
|
}
|
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
function tabcomplete() {
|
2017-11-09 12:44:57 +00:00
|
|
|
let fragment = clInput.value
|
2018-04-13 19:28:03 +01:00
|
|
|
let matches = state.cmdHistory.filter(key => key.startsWith(fragment))
|
2017-11-09 12:44:57 +00:00
|
|
|
let mostrecent = matches[matches.length - 1]
|
|
|
|
if (mostrecent != undefined) clInput.value = mostrecent
|
|
|
|
}
|
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
function history(n) {
|
|
|
|
HISTORY_SEARCH_STRING =
|
|
|
|
HISTORY_SEARCH_STRING === undefined
|
|
|
|
? clInput.value
|
|
|
|
: HISTORY_SEARCH_STRING
|
|
|
|
let matches = state.cmdHistory.filter(key =>
|
|
|
|
key.startsWith(HISTORY_SEARCH_STRING),
|
|
|
|
)
|
|
|
|
if (cmdline_history_position == 0) {
|
2017-11-15 13:41:04 -08:00
|
|
|
cmdline_history_current = clInput.value
|
2017-11-05 14:48:22 +00:00
|
|
|
}
|
2017-11-26 14:06:13 +00:00
|
|
|
let clamped_ind = matches.length + n - cmdline_history_position
|
|
|
|
clamped_ind = clamped_ind.clamp(0, matches.length)
|
2017-11-09 08:04:05 +00:00
|
|
|
|
2017-11-26 14:06:13 +00:00
|
|
|
const pot_history = matches[clamped_ind]
|
2018-04-13 19:28:03 +01:00
|
|
|
clInput.value =
|
|
|
|
pot_history == undefined ? cmdline_history_current : pot_history
|
2017-11-26 14:06:13 +00:00
|
|
|
|
|
|
|
// if there was no clampage, update history position
|
|
|
|
// there's a more sensible way of doing this but that would require more programmer time
|
2018-04-13 19:28:03 +01:00
|
|
|
if (clamped_ind == matches.length + n - cmdline_history_position)
|
|
|
|
cmdline_history_position = cmdline_history_position - n
|
2017-11-05 14:48:22 +00:00
|
|
|
}
|
|
|
|
|
2017-09-29 18:29:36 +01:00
|
|
|
/* Send the commandline to the background script and await response. */
|
|
|
|
function process() {
|
2017-12-01 12:12:56 +00:00
|
|
|
const command = getCompletion() || clInput.value
|
|
|
|
|
|
|
|
hide_and_clear()
|
2017-11-20 01:31:47 +00:00
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
const [func, ...args] = command.trim().split(/\s+/)
|
2018-04-25 22:30:57 +01:00
|
|
|
|
2018-04-27 16:45:50 +01:00
|
|
|
if (func.length === 0 || func.startsWith("#")) {
|
2018-04-25 22:30:57 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save non-secret commandlines to the history.
|
2018-04-13 19:28:03 +01:00
|
|
|
if (
|
|
|
|
!browser.extension.inIncognitoContext &&
|
|
|
|
!(func === "winopen" && args[0] === "-private")
|
2017-11-20 01:31:47 +00:00
|
|
|
) {
|
2017-12-01 12:12:56 +00:00
|
|
|
state.cmdHistory = state.cmdHistory.concat([command])
|
2017-11-09 08:19:28 +00:00
|
|
|
}
|
2017-11-05 14:48:22 +00:00
|
|
|
cmdline_history_position = 0
|
2017-11-22 18:05:54 +00:00
|
|
|
|
2017-12-01 12:12:56 +00:00
|
|
|
sendExstr(command)
|
2017-09-29 18:29:36 +01:00
|
|
|
}
|
2017-10-02 00:59:51 +01:00
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
export function fillcmdline(newcommand?: string, trailspace = true) {
|
2017-11-09 15:30:09 +00:00
|
|
|
if (newcommand !== "") {
|
|
|
|
if (trailspace) clInput.value = newcommand + " "
|
|
|
|
else clInput.value = newcommand
|
2017-10-06 04:16:02 +01:00
|
|
|
}
|
|
|
|
// Focus is lost for some reason.
|
2017-10-12 04:02:01 +01:00
|
|
|
focus()
|
2017-11-15 13:41:04 -08:00
|
|
|
isVisible = true
|
2018-04-13 19:28:03 +01:00
|
|
|
clInput.dispatchEvent(new Event("input")) // dirty hack for completions
|
2017-10-05 23:11:56 +01:00
|
|
|
}
|
|
|
|
|
2018-02-19 00:15:44 +00:00
|
|
|
/** Create a temporary textarea and give it to fn. Remove the textarea afterwards
|
|
|
|
|
|
|
|
Useful for document.execCommand
|
|
|
|
*/
|
2017-10-28 13:42:54 +01:00
|
|
|
function applyWithTmpTextArea(fn) {
|
|
|
|
let textarea
|
|
|
|
try {
|
|
|
|
textarea = document.createElement("textarea")
|
|
|
|
// Scratchpad must be `display`ed, but can be tiny and invisible.
|
|
|
|
// Being tiny and invisible means it won't make the parent page move.
|
2018-04-13 19:28:03 +01:00
|
|
|
textarea.style.cssText =
|
|
|
|
"visible: invisible; width: 0; height: 0; position: fixed"
|
2017-10-28 13:42:54 +01:00
|
|
|
textarea.contentEditable = "true"
|
|
|
|
document.documentElement.appendChild(textarea)
|
|
|
|
return fn(textarea)
|
|
|
|
} finally {
|
|
|
|
document.documentElement.removeChild(textarea)
|
|
|
|
}
|
2017-10-28 19:20:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export function setClipboard(content: string) {
|
2018-02-19 00:15:44 +00:00
|
|
|
applyWithTmpTextArea(scratchpad => {
|
2017-10-28 19:20:31 +08:00
|
|
|
scratchpad.value = content
|
|
|
|
scratchpad.select()
|
2017-10-28 13:42:54 +01:00
|
|
|
if (document.execCommand("Copy")) {
|
|
|
|
// // todo: Maybe we can consider to using some logger and show it with status bar in the future
|
2018-04-13 19:28:03 +01:00
|
|
|
logger.info("set clipboard:", scratchpad.value)
|
2017-10-28 13:42:54 +01:00
|
|
|
} else throw "Failed to copy!"
|
2017-10-28 19:20:31 +08:00
|
|
|
})
|
2018-02-19 00:15:44 +00:00
|
|
|
// Return focus to the document
|
2018-04-13 19:28:03 +01:00
|
|
|
Messaging.message("commandline_background", "hide")
|
2017-10-28 19:20:31 +08:00
|
|
|
}
|
|
|
|
|
2017-10-28 13:42:54 +01:00
|
|
|
export function getClipboard() {
|
2018-02-19 00:15:44 +00:00
|
|
|
const result = applyWithTmpTextArea(scratchpad => {
|
2017-10-28 19:20:31 +08:00
|
|
|
scratchpad.focus()
|
|
|
|
document.execCommand("Paste")
|
2017-10-28 13:42:54 +01:00
|
|
|
return scratchpad.textContent
|
2017-10-28 19:20:31 +08:00
|
|
|
})
|
2018-02-19 00:15:44 +00:00
|
|
|
// Return focus to the document
|
2018-04-13 19:28:03 +01:00
|
|
|
Messaging.message("commandline_background", "hide")
|
2018-02-19 00:15:44 +00:00
|
|
|
return result
|
2017-10-28 19:20:31 +08:00
|
|
|
}
|
|
|
|
|
2018-04-13 19:28:03 +01:00
|
|
|
Messaging.addListener("commandline_frame", Messaging.attributeCaller(SELF))
|