2018-11-02 06:26:24 +01:00
/ * * # C o m m a n d l i n e f u n c t i o n s
*
* This file contains functions to interact with the command line .
*
* If you want to bind them to keyboard shortcuts , be sure to prefix them with "ex." . For example , if you want to bind control - p to ` prev_completion ` , use :
*
* ` ` `
* bind -- mode = ex < C - p > ex . prev_completion
* ` ` `
*
2018-11-08 07:00:35 +01:00
* Note that you can also bind Tridactyl ' s [ editor functions ] ( / s t a t i c / d o c s / m o d u l e s / _ s r c _ l i b _ e d i t o r _ . h t m l ) i n t h e c o m m a n d l i n e .
2018-11-02 06:26:24 +01:00
*
* Contrary to the main tridactyl help page , this one doesn 't tell you whether a specific function is bound to something. For now, you' ll have to make do with ` :bind ` and ` :viewconfig ` .
*
* /
/** ignore this line */
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
2018-10-04 13:59:19 +01:00
import * as perf from "@src/perf"
import "@src/lib/number.clamp"
2018-09-29 17:38:58 -07:00
import "@src/lib/html-tagged-template"
2018-11-19 07:14:08 +01:00
import { TabAllCompletionSource } from "@src/completions/TabAll"
2020-02-27 21:31:41 +08:00
import { BindingsCompletionSource } from "@src/completions/Bindings"
2018-11-20 20:16:07 +00:00
import { BufferCompletionSource } from "@src/completions/Tab"
2018-09-29 17:38:58 -07:00
import { BmarkCompletionSource } from "@src/completions/Bmark"
import { ExcmdCompletionSource } from "@src/completions/Excmd"
2020-02-27 13:53:06 +00:00
import { CompositeCompletionSource } from "@src/completions/Composite"
2018-10-11 16:55:01 +02:00
import { FileSystemCompletionSource } from "@src/completions/FileSystem"
2019-02-23 16:17:05 +01:00
import { GuisetCompletionSource } from "@src/completions/Guiset"
2018-10-10 11:31:27 +02:00
import { HelpCompletionSource } from "@src/completions/Help"
2019-11-06 11:26:52 +00:00
import { AproposCompletionSource } from "@src/completions/Apropos"
2018-09-29 17:38:58 -07:00
import { HistoryCompletionSource } from "@src/completions/History"
2018-10-12 12:44:14 +02:00
import { PreferenceCompletionSource } from "@src/completions/Preferences"
2018-12-27 11:05:47 +01:00
import { RssCompletionSource } from "@src/completions/Rss"
2019-01-19 18:06:05 +01:00
import { SessionsCompletionSource } from "@src/completions/Sessions"
2018-09-29 17:38:58 -07:00
import { SettingsCompletionSource } from "@src/completions/Settings"
2019-01-31 08:13:05 +01:00
import { WindowCompletionSource } from "@src/completions/Window"
2019-09-14 02:54:50 +05:30
import { ExtensionsCompletionSource } from "@src/completions/Extensions"
2018-09-29 16:17:52 -07:00
import * as Messaging from "@src/lib/messaging"
2018-09-29 16:23:06 -07:00
import "@src/lib/number.clamp"
2018-09-29 17:38:58 -07:00
import state from "@src/state"
2020-03-03 09:41:04 +00:00
import * as State from "@src/state"
2018-09-29 15:30:48 -07:00
import Logger from "@src/lib/logging"
2018-09-29 16:43:18 -07:00
import { theme } from "@src/content/styling"
2018-10-04 13:59:19 +01:00
2018-10-31 08:09:51 +01:00
import * as genericParser from "@src/parsers/genericmode"
import * as tri_editor from "@src/lib/editor"
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-04-13 19:28:03 +01:00
const logger = new Logger ( "cmdline" )
2017-10-05 23:11:56 +01:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2019-04-20 23:57:59 +02:00
const commandline_state = {
activeCompletions : undefined ,
clInput : ( window . document . getElementById ( "tridactyl-input" ) as HTMLInputElement ) ,
clear ,
cmdline_history_position : 0 ,
completionsDiv : window.document.getElementById ( "completions" ) ,
fns : undefined ,
getCompletion ,
history ,
/ * * @ h i d d e n
* 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
* /
isVisible : false ,
2019-10-20 13:09:21 +02:00
keyEvents : new Array < KeyEventLike > ( ) ,
2019-04-20 23:57:59 +02:00
refresh_completions ,
state ,
}
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" ) )
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-22 18:05:54 +00:00
function resizeArea() {
2019-04-20 23:57:59 +02:00
if ( commandline_state . isVisible ) {
2018-10-02 12:44:35 +02:00
Messaging . messageOwnTab ( "commandline_content" , "show" )
Messaging . messageOwnTab ( "commandline_content" , "focus" )
focus ( )
2017-11-22 18:05:54 +00:00
}
}
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* This is a bit loosely defined at the moment .
* Should work so long as there ' s only one completion source per prefix .
* /
2017-11-22 18:13:31 +00:00
function getCompletion() {
2019-04-20 23:57:59 +02:00
if ( ! commandline_state . activeCompletions ) return undefined
2018-10-02 16:40:38 +02:00
2019-04-20 23:57:59 +02:00
for ( const comp of commandline_state . 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
}
}
}
2019-04-20 23:57:59 +02:00
commandline_state . getCompletion = getCompletion
2017-11-22 18:13:31 +00:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-10-31 08:09:51 +01:00
export function enableCompletions() {
2019-04-20 23:57:59 +02:00
if ( ! commandline_state . activeCompletions ) {
commandline_state . activeCompletions = [
2019-01-15 17:51:20 +00:00
// FindCompletionSource,
2020-02-27 21:31:41 +08:00
BindingsCompletionSource ,
2018-12-10 07:00:11 +01:00
BmarkCompletionSource ,
TabAllCompletionSource ,
BufferCompletionSource ,
ExcmdCompletionSource ,
2020-02-27 13:53:06 +00:00
CompositeCompletionSource ,
2018-12-10 07:00:11 +01:00
FileSystemCompletionSource ,
2019-02-23 16:17:05 +01:00
GuisetCompletionSource ,
2018-12-10 07:00:11 +01:00
HelpCompletionSource ,
2019-11-06 11:26:52 +00:00
AproposCompletionSource ,
2018-12-10 07:00:11 +01:00
HistoryCompletionSource ,
PreferenceCompletionSource ,
2018-12-27 11:05:47 +01:00
RssCompletionSource ,
2019-01-19 18:06:05 +01:00
SessionsCompletionSource ,
SettingsCompletionSource ,
2019-01-31 08:13:05 +01:00
WindowCompletionSource ,
2019-09-14 02:54:50 +05:30
ExtensionsCompletionSource ,
2017-11-22 18:05:54 +00:00
]
2018-12-10 07:00:11 +01:00
. map ( constructorr = > {
try {
2019-04-20 23:57:59 +02:00
return new constructorr ( commandline_state . completionsDiv )
2018-12-10 07:00:11 +01:00
} catch ( e ) { }
} )
. filter ( c = > c )
2017-11-15 13:41:04 -08:00
2017-11-22 18:13:31 +00:00
const fragment = document . createDocumentFragment ( )
2019-04-20 23:57:59 +02:00
commandline_state . activeCompletions . forEach ( comp = > fragment . appendChild ( comp . node ) )
commandline_state . completionsDiv . appendChild ( fragment )
logger . debug ( commandline_state . activeCompletions )
2017-11-22 18:05:54 +00:00
}
}
2017-11-22 18:13:31 +00:00
/* document.addEventListener("DOMContentLoaded", enableCompletions) */
2018-11-02 06:26:24 +01:00
/** @hidden **/
2019-04-20 23:57:59 +02:00
const noblur = e = > setTimeout ( ( ) = > commandline_state . clInput . focus ( ) , 0 )
2017-11-25 23:15:45 +00:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-22 18:13:31 +00:00
export function focus() {
2019-04-20 23:57:59 +02:00
commandline_state . clInput . focus ( )
commandline_state . clInput . removeEventListener ( "blur" , noblur )
commandline_state . clInput . addEventListener ( "blur" , noblur )
2017-11-22 18:13:31 +00:00
}
2017-09-29 18:29:36 +01:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-26 14:06:13 +00:00
let HISTORY_SEARCH_STRING : string
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* Command line keybindings
* * /
2019-04-13 09:46:23 +02:00
const keyParser = keys = > genericParser . parser ( "exmaps" , keys )
2018-11-02 06:26:24 +01:00
/** @hidden **/
2019-02-09 13:01:31 +01:00
let history_called = false
/** @hidden **/
let prev_cmd_called_history = false
/** @hidden **/
2019-04-20 23:57:59 +02:00
commandline_state . clInput . addEventListener (
2018-11-04 12:28:31 +00:00
"keydown" ,
function ( keyevent : KeyboardEvent ) {
This allowed malicious web pages to send artificial key events to
the parsers for all modes except the command line (which has
always been protected inside an iframe).
If the native messenger was not installed, the bug could not be
exploited for any more than nuisance attacks (closing tabs,
quitting Firefox, etc.). If the native messenger was installed,
an attack using the mpv hint mode (bound to `;v` by default) and
a specially crafted link would allow an attacker to execute some
commands in the user's shell. Due to the way hyperlinks are
encoded, it would require more cunning than the Tridactyl
developers possess to usefully exploit as it is difficult to pass
arguments to commands.
This did mean that the standard output of mpv (including the
attacker's URL) was also available to an attacker via pipes. We
are not aware of any way to abuse that with commonly installed
utilities.
We are unaware of any pages exploiting this in the wild.
Nevertheless, this security regression should not have happened.
A short incident report follows:
These checks were accidentally removed when key handling was
rewritten in September 2018. The PR was reviewed, but it was a
large PR and the regression was missed by the reviewers.
We became aware of the regression after a question in our support
chat prompted @glacambre to check on exactly how we were using
`isTrusted` and they realised that we weren't using it any more.
We will shortly introduce automated testing to check these
security properties that we rely on.
We will consider adding a check to continuous integration that
flags any change to files containing security relevant code for
more detailed review.
Affected versions: - Tridactyl 1.14.0 - 1.14.10, 1.15.0.
Mitigation:
- Update to Tridactyl 1.16.0+ or 1.14.13+
- If updating is unfeasible, we recommend removing the native
messenger by running `:! pwd` in Tridactyl and then deleting that
directory from your filesystem.
- If you've thought of a clever exploit, please contact
bovine3dom or cmcaine privately on Matrix or by email.
2019-06-14 10:13:19 +01:00
if ( ! keyevent . isTrusted ) return
2019-04-20 23:57:59 +02:00
commandline_state . keyEvents . push ( keyevent )
const response = keyParser ( commandline_state . keyEvents )
2018-11-04 12:28:31 +00:00
if ( response . isMatch ) {
2017-11-22 23:01:34 +00:00
keyevent . preventDefault ( )
2018-11-04 12:28:31 +00:00
keyevent . stopImmediatePropagation ( )
2019-02-09 13:01:31 +01:00
} else {
// Ideally, all keys that aren't explicitly bound to an ex command
// should be bound to a "self-insert" command that would input the
// key itself. Because it's not possible to generate events as if
// they originated from the user, we can't do this, but we still
// need to simulate it, in order to have history() work.
prev_cmd_called_history = false
2018-11-04 12:28:31 +00:00
}
2019-06-04 18:10:45 +01:00
if ( response . value ) {
2019-04-20 23:57:59 +02:00
commandline_state . keyEvents = [ ]
2019-02-09 13:01:31 +01:00
history_called = false
2019-04-21 01:38:57 -07:00
2019-06-04 10:29:12 +01:00
// If excmds start with 'ex.' they're coming back to us anyway, so skip that.
2019-06-04 18:10:45 +01:00
// This is definitely a hack. Should expand aliases with exmode, etc.
// but this whole thing should be scrapped soon, so whatever.
if ( response . value . startsWith ( "ex." ) ) {
const funcname = response . value . slice ( 3 )
2019-06-04 10:29:12 +01:00
commandline_state . fns [ funcname ] ( )
prev_cmd_called_history = history_called
} else {
// Send excmds directly to our own tab, which fixes the
// old bug where a command would be issued in one tab but
// land in another because the active tab had
// changed. Background-mode excmds will be received by the
// own tab's content script and then bounced through a
// shim to the background, but the latency increase should
// be acceptable becuase the background-mode excmds tend
// to be a touch less latency-sensitive.
Messaging . messageOwnTab ( "controller_content" , "acceptExCmd" , [
2019-06-04 18:10:45 +01:00
response . value ,
2019-06-04 10:29:12 +01:00
] ) . then ( _ = > ( prev_cmd_called_history = history_called ) )
}
2018-11-04 12:28:31 +00:00
} else {
2019-04-20 23:57:59 +02:00
commandline_state . keyEvents = response . keys
2018-11-04 12:28:31 +00:00
}
} ,
true ,
)
2017-11-26 14:06:13 +00:00
2018-12-24 09:18:34 +01:00
export function refresh_completions ( exstr ) {
2019-04-20 23:57:59 +02:00
if ( ! commandline_state . activeCompletions ) enableCompletions ( )
2018-12-24 09:18:34 +01:00
return Promise . all (
2019-04-20 23:57:59 +02:00
commandline_state . activeCompletions . map ( comp = >
2019-03-01 05:58:20 +01:00
comp . filter ( exstr ) . then ( ( ) = > {
if ( comp . shouldRefresh ( ) ) {
return resizeArea ( )
}
} ) ,
) ,
2019-02-12 12:43:53 +01:00
) . catch ( err = > {
console . error ( err )
return [ ]
} ) // We can't use the regular logging mechanism because the user is using the command line.
2018-10-31 08:09:51 +01:00
}
2017-09-29 18:29:36 +01:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-10-11 16:55:01 +02:00
let onInputPromise : Promise < any > = Promise . resolve ( )
2018-11-02 06:26:24 +01:00
/** @hidden **/
2019-04-20 23:57:59 +02:00
commandline_state . clInput . addEventListener ( "input" , ( ) = > {
const exstr = commandline_state . clInput . value
2018-10-24 21:27:56 +02:00
// Schedule completion computation. We do not start computing immediately because this would incur a slow down on quickly repeated input events (e.g. maintaining <Backspace> pressed)
2019-02-04 20:06:05 +01:00
setTimeout ( async ( ) = > {
2019-02-12 12:43:53 +01:00
// Make sure the previous computation has ended
await onInputPromise
2018-10-24 21:27:56 +02:00
// If we're not the current completion computation anymore, stop
2019-04-20 23:57:59 +02:00
if ( exstr !== commandline_state . clInput . value ) return
2018-10-24 21:27:56 +02:00
2018-12-24 09:18:34 +01:00
onInputPromise = refresh_completions ( exstr )
2019-03-05 06:43:53 +01:00
} , 100 )
2017-11-15 13:41:04 -08:00
} )
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-05 14:48:22 +00:00
let cmdline_history_current = ""
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* Clears the command line .
* If you intend to close the command line after this , set evlistener to true in order to enable losing focus .
2018-07-05 19:50:32 +02:00
* Otherwise , no need to pass an argument .
* /
export function clear ( evlistener = false ) {
2019-04-20 23:57:59 +02:00
if ( evlistener ) commandline_state . clInput . removeEventListener ( "blur" , noblur )
commandline_state . clInput . value = ""
commandline_state . cmdline_history_position = 0
2017-11-26 14:06:13 +00:00
cmdline_history_current = ""
2018-07-05 19:50:32 +02:00
}
2019-04-20 23:57:59 +02:00
commandline_state . clear = clear
2018-11-02 06:26:24 +01:00
/** @hidden **/
2020-03-03 09:41:04 +00:00
async function history ( n ) {
2019-02-09 13:01:31 +01:00
history_called = true
if ( ! prev_cmd_called_history ) {
2019-04-20 23:57:59 +02:00
HISTORY_SEARCH_STRING = commandline_state . clInput . value
2019-02-09 13:01:31 +01:00
}
2020-03-03 09:41:04 +00:00
const matches = ( await State . getAsync ( "cmdHistory" ) ) . filter ( key = >
2018-04-13 19:28:03 +01:00
key . startsWith ( HISTORY_SEARCH_STRING ) ,
)
2019-04-20 23:57:59 +02:00
if ( commandline_state . cmdline_history_position === 0 ) {
cmdline_history_current = commandline_state . clInput . value
2017-11-05 14:48:22 +00:00
}
2019-04-20 23:57:59 +02:00
let clamped_ind = matches . length + n - commandline_state . cmdline_history_position
2017-11-26 14:06:13 +00:00
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 ]
2019-04-20 23:57:59 +02:00
commandline_state . clInput . value =
2019-04-10 07:21:21 +02:00
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
2019-04-20 23:57:59 +02:00
if ( clamped_ind === matches . length + n - commandline_state . cmdline_history_position )
commandline_state . cmdline_history_position = commandline_state . cmdline_history_position - n
2017-09-29 18:29:36 +01:00
}
2019-04-20 23:57:59 +02:00
commandline_state . history = history
2017-10-02 00:59:51 +01:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-06-30 16:59:10 +02:00
export function fillcmdline (
newcommand? : string ,
trailspace = true ,
ffocus = true ,
) {
2019-04-20 23:57:59 +02:00
if ( trailspace ) commandline_state . clInput . value = newcommand + " "
else commandline_state . clInput . value = newcommand
commandline_state . isVisible = true
2018-12-24 09:18:34 +01:00
let result = Promise . resolve ( [ ] )
2018-06-30 16:59:10 +02:00
// Focus is lost for some reason.
if ( ffocus ) {
focus ( )
2019-04-20 23:57:59 +02:00
result = refresh_completions ( commandline_state . clInput . value )
2018-06-30 16:59:10 +02:00
}
2018-12-24 09:18:34 +01:00
return result
2017-10-05 23:11:56 +01:00
}
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* 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
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-07-26 10:17:46 +01:00
export async function setClipboard ( content : string ) {
2018-11-07 06:13:43 +01:00
await Messaging . messageOwnTab ( "commandline_content" , "focus" )
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-11-07 06:13:43 +01:00
await Messaging . messageOwnTab ( "commandline_content" , "hide" )
return Messaging . messageOwnTab ( "commandline_content" , "blur" )
2017-10-28 19:20:31 +08:00
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-11-07 06:13:43 +01:00
export async function getClipboard() {
await Messaging . messageOwnTab ( "commandline_content" , "focus" )
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-11-07 06:13:43 +01:00
await Messaging . messageOwnTab ( "commandline_content" , "hide" )
await Messaging . messageOwnTab ( "commandline_content" , "blur" )
2018-02-19 00:15:44 +00:00
return result
2017-10-28 19:20:31 +08:00
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-07-01 18:17:35 +02:00
export function getContent() {
2019-04-20 23:57:59 +02:00
return commandline_state . clInput . value
2018-07-01 18:17:35 +02:00
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-12-04 07:12:26 +01:00
export function editor_function ( fn_name , . . . args ) {
2018-12-24 09:18:34 +01:00
let result = Promise . resolve ( [ ] )
2018-10-31 08:09:51 +01:00
if ( tri_editor [ fn_name ] ) {
2019-04-20 23:57:59 +02:00
tri_editor [ fn_name ] ( commandline_state . clInput , . . . args )
result = refresh_completions ( commandline_state . clInput . value )
2018-10-31 08:09:51 +01:00
} else {
// The user is using the command line so we can't log message there
// logger.error(`No editor function named ${fn_name}!`)
console . error ( ` No editor function named ${ fn_name } ! ` )
}
2018-12-24 09:18:34 +01:00
return result
2018-10-31 08:09:51 +01:00
}
import * as SELF from "@src/commandline_frame"
2018-04-13 19:28:03 +01:00
Messaging . addListener ( "commandline_frame" , Messaging . attributeCaller ( SELF ) )
2018-09-03 03:13:42 -07:00
2019-04-20 23:57:59 +02:00
import { getCommandlineFns } from "@src/lib/commandline_cmds"
2019-10-20 13:09:21 +02:00
import { KeyEventLike } from "./lib/keyseq"
2019-04-20 23:57:59 +02:00
commandline_state . fns = getCommandlineFns ( commandline_state )
Messaging . addListener ( "commandline_cmd" , Messaging . attributeCaller ( commandline_state . fns ) )
2018-09-05 00:13:25 -07:00
// Listen for statistics from the commandline iframe and send them to
// the background for collection. Attach the observer to the window
// object since there's apparently a bug that causes performance
// observers to be GC'd even if they're still the target of a
// callback.
2019-04-12 05:54:31 +02:00
; ( window as any ) . tri = Object . assign ( window . tri || { } , {
2018-09-05 00:13:25 -07:00
perfObserver : perf.listenForCounters ( ) ,
2018-09-03 03:13:42 -07:00
} )