-
-
Notifications
You must be signed in to change notification settings - Fork 410
/
Copy pathexcmds.ts
6210 lines (5536 loc) · 240 KB
/
excmds.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* eslint-disable spaced-comment */
// '//#' is a start point for a simple text-replacement-type macro. See excmds_macros.py
/** # Tridactyl help page
Use `:help <excmd>` or scroll down to show [[help]] for a particular excmd. If you're still stuck, you might consider reading through the [:tutor](/static/clippy/1-tutor.html) again.
The default keybinds and settings can be found [here](/static/docs/classes/_src_lib_config_.default_config.html) and active binds can be seen with `:viewconfig nmaps` or with [[bind]].
Tridactyl also provides a few functions to manipulate text in the command line or text areas that can be found [here](/static/docs/modules/_src_lib_editor_.html). There are also a few commands only available in the command line which can be found [here](/static/docs/modules/_src_commandline_frame_.html).
Ex-commands available exclusively in hint mode are listed [here](/static/docs/modules/_src_content_hinting_.html)
We also have a [wiki](https://github.com/tridactyl/tridactyl/wiki) which may be edited by anyone.
## How to use this help page
Every function (excmd) on this page can be called via Tridactyl's command line which we call "ex". There is a slight change in syntax, however. Wherever you see:
`function(arg1,arg2)`
You should instead type
`function arg1 arg2` into the Tridactyl command line (accessed via `:`)
A "splat" operator (...) means that the excmd will accept any number of space-delimited arguments into that parameter.
Above each function signature you will see any aliases or key sequences bound to it. The internal names for the various modes are used, which are listed here:
- `nmaps`: normal mode binds
- `imaps`: insert mode binds
- `inputmaps`: input mode binds
- `ignoremaps`: ignore mode binds
- `exaliases`: aliases in the command mode
- `exmaps`: commandline mode binds
At the bottom of each function's help page, you can click on a link that will take you straight to that function's definition in our code.
You do not need to worry about types. Return values which are promises will turn into whatever they promise to when used in [[composite]].
## Caveats
There are some caveats common to all webextension vimperator-alikes:
- To make Tridactyl work on addons.mozilla.org and some other Mozilla domains, you need to open `about:config` and add a new boolean `privacy.resistFingerprinting.block_mozAddonManager` with the value `true`, as well as remove those domains from `extensions.webextensions.restrictedDomains`.
- Tridactyl can't run on about:\*, some file:\* URIs, view-source:\*, or data:\*, URIs.
- To change/hide the GUI of Firefox from Tridactyl, you can use [[guiset]] with the native messenger installed (see [[native]] and [[nativeinstall]]). Alternatively, you can edit your userChrome yourself.
## Getting help
For more information, and FAQs, check out our [readme][2] and [troubleshooting guide][3] on github.
Tridactyl is in a pretty early stage of development. Please report any issues and make requests for missing features on the GitHub [project page][1]. You can also get in touch using Matrix, Gitter, or IRC chat clients:
[![Matrix Chat][matrix-badge]][matrix-link]
[![Gitter Chat][gitter-badge]][gitter-link]
[![Libera Chat][libera-badge]][libera-link]
All three channels are mirrored together, so it doesn't matter which one you use.
[1]: https://github.com/tridactyl/tridactyl/issues
[2]: https://github.com/tridactyl/tridactyl#readme
[3]: https://github.com/tridactyl/tridactyl/blob/master/doc/troubleshooting.md
[gitter-badge]: /static/badges/gitter-badge.svg
[gitter-link]: https://gitter.im/tridactyl/Lobby
[libera-badge]: /static/badges/libera-badge.svg
[libera-link]: ircs://irc.libera.chat:6697/tridactyl
[matrix-badge]: /static/badges/matrix-badge.svg
[matrix-link]: https://matrix.to/#/#tridactyl:matrix.org
*/
/** ignore this line */
// {{{ setup
// Shared
import * as Messaging from "@src/lib/messaging"
import { ownWinTriIndex, getTriVersion, browserBg, activeTab, activeTabOnWindow, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow, openInTab, queryAndURLwrangler, goToTab, getSortedTabs, prevActiveTab } from "@src/lib/webext"
import * as Container from "@src/lib/containers"
import state from "@src/state"
import * as State from "@src/state"
import { contentState, ModeName } from "@src/content/state_content"
import * as UrlUtil from "@src/lib/url_util"
import * as config from "@src/lib/config"
import * as aliases from "@src/lib/aliases"
import * as Logging from "@src/lib/logging"
import { AutoContain } from "@src/lib/autocontainers"
import * as CSS from "css"
import * as Perf from "@src/perf"
import * as Metadata from "@src/.metadata.generated"
import { ObjectType } from "../compiler/types/ObjectType"
import * as Native from "@src/lib/native"
import * as TTS from "@src/lib/text_to_speech"
import * as excmd_parser from "@src/parsers/exmode"
import * as escape from "@src/lib/escape"
import semverCompare from "semver-compare"
import * as hint_util from "@src/lib/hint_util"
import { OpenMode } from "@src/lib/hint_util"
import * as Proxy from "@src/lib/proxy"
import * as arg from "@src/lib/arg_util"
import * as R from "ramda"
/**
* This is used to drive some excmd handling in `composite`.
*
* @hidden
*/
let ALL_EXCMDS
// The entry-point script will make sure this has the right set of
// excmds, so we can use it without futher configuration.
import * as controller from "@src/lib/controller"
//#content_helper
import { generator as KEY_MUNCHER } from "@src/content/controller_content"
/**
* Used to store the types of the parameters for each excmd for
* self-documenting functionality.
*
* @hidden
*/
export const cmd_params = new Map<string, Map<string, string>>()
/** @hidden */
const logger = new Logging.Logger("excmd")
/** @hidden **/
const TRI_VERSION = getTriVersion()
//#content_helper
// {
import "@src/lib/number.clamp"
import * as CTSELF from "@src/.excmds_content.generated"
import { CmdlineCmds as CtCmdlineCmds } from "@src/background/commandline_cmds"
import { EditorCmds as CtEditorCmds } from "@src/background/editor"
import * as DOM from "@src/lib/dom"
import * as CommandLineContent from "@src/content/commandline_content"
import * as scrolling from "@src/content/scrolling"
import { ownTab } from "@src/lib/webext"
import { rot13_helper, jumble_helper } from "@src/lib/editor_utils"
import * as finding from "@src/content/finding"
import * as toys from "./content/toys"
import * as hinting from "@src/content/hinting"
import * as gobbleMode from "@src/parsers/gobblemode"
import * as nMode from "@src/parsers/nmode"
ALL_EXCMDS = {
"": CTSELF,
ex: CtCmdlineCmds,
text: CtEditorCmds,
}
// }
import { mapstrToKeyseq, mozMapToMinimalKey, minimalKeyToMozMap } from "@src/lib/keyseq"
//#background_helper
// {
// tslint:disable-next-line:no-unused-declaration
import "@src/lib/number.mod"
import * as BGSELF from "@src/.excmds_background.generated"
import { CmdlineCmds as BgCmdlineCmds } from "@src/background/commandline_cmds"
import { EditorCmds as BgEditorCmds } from "@src/background/editor"
import { EditorCmds } from "@src/background/editor"
import { firefoxVersionAtLeast } from "@src/lib/webext"
import { parse_bind_args, modeMaps } from "@src/lib/binding"
import * as rc from "@src/background/config_rc"
import * as css_util from "@src/lib/css_util"
import * as Updates from "@src/lib/updates"
import * as Extensions from "@src/lib/extension_info"
import * as webrequests from "@src/background/webrequests"
import * as commandsHelper from "@src/background/commands"
import { tgroups, tgroupActivate, setTabTgroup, setWindowTgroup, setTgroups, windowTgroup, windowLastTgroup, tgroupClearOldInfo, tgroupLastTabId, tgroupTabs, clearAllTgroupInfo, tgroupActivateLast, tgroupHandleTabActivated, tgroupHandleTabCreated, tgroupHandleTabAttached, tgroupHandleTabUpdated, tgroupHandleTabRemoved, tgroupHandleTabDetached } from "./lib/tab_groups"
ALL_EXCMDS = {
"": BGSELF,
ex: BgCmdlineCmds,
text: BgEditorCmds,
}
/** @hidden */
// }
// }}}
// {{{ Native messenger stuff
/** @hidden **/
//#background
export async function getNativeVersion(): Promise<string> {
return Native.getNativeMessengerVersion()
}
/** @hidden
* This function is used by rssexec and rssexec completions.
*/
//#content
export async function getRssLinks(): Promise<Array<{ type: string; url: string; title: string }>> {
const seen = new Set<string>()
return Array.from(document.querySelectorAll<HTMLAnchorElement | HTMLLinkElement>("a, link[rel='alternate']"))
.filter(e => typeof e.href === "string")
.reduce((acc, e) => {
let type = ""
// Start by detecting type because url doesn't necessarily contain the words "rss" or "atom"
if (e.type) {
// if type doesn't match either rss or atom, don't include link
if (e.type.indexOf("rss") < 0 && e.type.indexOf("atom") < 0) return acc
type = e.type
} else {
// Making sure that we match either a dot or "xml" because "urss" and "atom" are actual words
if (/(\.rss)|(rss\.xml)/i.test(e.href)) type = "application/rss+xml"
else if (/(\.atom)|(atom\.xml)/i.test(e.href)) type = "application/atom+xml"
else return acc
}
if (seen.has(e.href)) return acc
seen.add(e.href)
return acc.concat({ type, url: e.href, title: e.title || e.innerText } as { type: string; url: string; title: string })
}, [])
}
/**
* Execute [[rsscmd]] for an rss link.
*
* If `url` is undefined, Tridactyl will look for rss links in the current
* page. If it doesn't find any, it will display an error message. If it finds
* multiple urls, it will offer completions in order for you to select the link
* you're interested in. If a single rss feed is found, it will automatically
* be selected.
*/
//#content
export async function rssexec(url: string, type?: string, ...title: string[]) {
if (!url || url === "") {
const links = await getRssLinks()
switch (links.length) {
case 0:
throw new Error("No rss link found on this page.")
break
case 1:
url = links[0].url
title = [links[0].title]
type = links[0].type
break
default:
return fillcmdline("rssexec")
}
}
let rsscmd = config.get("rsscmd")
if (rsscmd.match("%[uty]")) {
rsscmd = rsscmd
.replace("%u", url)
.replace("%t", title.join(" "))
.replace("%y", type || "")
} else {
rsscmd += " " + url
}
// Need actual excmd parsing here.
return controller.acceptExCmd(rsscmd)
}
/**
* Fills the element matched by `selector` with content and falls back to the last used input if the element can't be found. You probably don't want this; it used to be used internally for [[editor]].
*
* That said, `bind gs fillinput null [Tridactyl](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/) is my favourite add-on` could probably come in handy.
*/
//#content
export function fillinput(selector: string, ...content: string[]) {
let inputToFill = document.querySelector(selector)
if (!inputToFill) inputToFill = DOM.getLastUsedInput()
// CodeMirror support (I think only versions prior to CodeMirror 6)
if (inputToFill?.parentNode?.parentElement?.className?.match(/CodeMirror/gi)) {
;(inputToFill.parentNode.parentElement as any).wrappedJSObject.CodeMirror.setValue(content.join(" "))
return
}
if ("value" in inputToFill) {
;(inputToFill as HTMLInputElement).value = content.join(" ")
} else {
inputToFill.textContent = content.join(" ")
}
}
/** @hidden */
//#content_helper
export function getInput(e: HTMLElement) {
// this should probably be subsumed by the focusinput code
if ("value" in e) {
return (e as HTMLInputElement).value
} else {
return e.textContent
}
}
/** @hidden */
//#content
export function getinput() {
return getInput(DOM.getLastUsedInput())
}
/** @hidden */
//#content
export function getInputSelector() {
return DOM.getSelector(DOM.getLastUsedInput())
}
/** @hidden */
//#content
export function addTridactylEditorClass(selector: string) {
document.querySelector(selector)?.classList.add("TridactylEditing")
}
/** @hidden */
//#content
export function removeTridactylEditorClass(selector: string) {
document.querySelector(selector)?.classList.remove("TridactylEditing")
}
//#content_helper
import { getEditor } from "editor-adapter"
/**
* Opens your favourite editor (which is currently gVim) and fills the last used input with whatever you write into that file.
* **Requires that the native messenger is installed, see [[native]] and [[nativeinstall]]**.
*
* Uses the `editorcmd` config option, default = `auto` looks through a list defined in lib/native.ts try find a sensible combination. If it's a bit slow, or chooses the wrong editor, or gives up completely, set editorcmd to something you want. The command must stay in the foreground until the editor exits.
*
* The editorcmd needs to accept a filename, stay in the foreground while it's edited, save the file and exit. By default the filename is added to the end of editorcmd, if you require control over the position of that argument, the first occurrence of %f in editorcmd is replaced with the filename. %l, if it exists, is replaced with the line number of the cursor and %c with the column number. For example:
* ```
* set editorcmd terminator -u -e "vim %f '+normal!%lGzv%c|'"
* ```
*
* You're probably better off using the default insert mode bind of `<C-i>` (Ctrl-i) to access this.
*
* This function returns a tuple containing the path to the file that was opened by the editor and its content. This enables creating commands such as the following one, which deletes the temporary file created by the editor:
* ```
* alias editor_rm composite editor | jsb -p tri.native.run(`rm -f '${JS_ARG[0]}'`)
* bind --mode=insert <C-i> editor_rm
* bind --mode=input <C-i> editor_rm
* ```
*/
//#content
export async function editor() {
const elem = DOM.getLastUsedInput()
const selector = DOM.getSelector(elem)
addTridactylEditorClass(selector)
if (!(await Native.nativegate())) {
removeTridactylEditorClass(selector)
return undefined
}
const beforeUnloadListener = (event: BeforeUnloadEvent) => {
event.preventDefault()
event.returnValue = true
}
window.addEventListener("beforeunload", beforeUnloadListener)
let ans
try {
const editor = getEditor(elem, { preferHTML: true })
const text = await editor.getContent()
const pos = await editor.getCursor()
const file = (await Native.temp(text, document.location.hostname)).content
const exec = await Native.editor(file, ...pos)
if (exec.code == 0) {
await editor.setContent(exec.content)
// TODO: ask the editor nicely where its cursor was left and use that
// for now just try to put it where it started at
await editor.setCursor(...pos)
// TODO: add annoying "This message was written with [Tridactyl](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/)" to everything written using editor
ans = [file, exec.content]
} else {
logger.debug(`Editor terminated with non-zero exit code: ${exec.code}`)
}
} catch (e) {
throw new Error(`:editor failed: ${e}`)
} finally {
removeTridactylEditorClass(selector)
window.removeEventListener("beforeunload", beforeUnloadListener)
return ans
}
}
/**
* Like [[guiset]] but quieter.
*/
//#background
export async function guiset_quiet(rule: string, option: string) {
if (!rule || !option) throw new Error(":guiset requires two arguments. See `:help guiset` for more information.")
if (rule == "navbar" && option == "none") throw new Error("`:guiset navbar none` is currently broken, see https://github.com/tridactyl/tridactyl/issues/1728")
// Could potentially fall back to sending minimal example to clipboard if native not installed
// Check for native messenger and make sure we have a plausible profile directory
if (!(await Native.nativegate("0.1.1"))) return
const profile_dir = await Native.getProfileDir()
await setpref("toolkit.legacyUserProfileCustomizations.stylesheets", "true")
// Make backups
await Native.mkdir(profile_dir + "/chrome", true)
const cssstr = (await Native.read(profile_dir + "/chrome/userChrome.css")).content
const cssstrOrig = (await Native.read(profile_dir + "/chrome/userChrome.orig.css")).content
if (cssstrOrig === "") await Native.write(profile_dir + "/chrome/userChrome.orig.css", cssstr)
await Native.write(profile_dir + "/chrome/userChrome.css.tri.bak", cssstr)
// Modify and write new CSS
const stylesheet = CSS.parse(cssstr, { silent: true })
if (stylesheet.stylesheet.parsingErrors.length) {
const error = stylesheet.stylesheet.parsingErrors[0]
throw new Error(`Your current userChrome.css is malformed: ${error.reason} at ${error.line}:${error.column}. Fix or delete it and try again.`)
}
// Trim due to https://github.com/reworkcss/css/issues/113
const stylesheetDone = CSS.stringify(css_util.changeCss(rule, option, stylesheet)).trim()
return Native.write(profile_dir + "/chrome/userChrome.css", stylesheetDone)
}
/**
* Change which parts of the Firefox user interface are shown. **NB: This feature is experimental and might break stuff.**
*
* Might mangle your userChrome. Requires native messenger, and you must restart Firefox each time to see any changes (this can be done using [[restart]]). <!-- (unless you enable addon debugging and refresh using the browser toolbox) -->
*
* Also flips the preference `toolkit.legacyUserProfileCustomizations.stylesheets` to true so that FF will read your userChrome.
*
* View available rules and options [here](/static/docs/modules/_src_lib_css_util_.html#potentialrules) and [here](/static/docs/modules/_src_lib_css_util_.html#metarules).
*
* Example usage: `guiset gui none`, `guiset gui full`, `guiset tabs autohide`.
*
* Some of the available options:
*
* - gui
* - full
* - none
*
* - tabs
* - always
* - autohide
*
* - navbar
* - always
* - autohide
* - none
*
* - hoverlink (the little link that appears when you hover over a link)
* - none
* - left
* - right
* - top-left
* - top-right
*
* - statuspanel (hoverlink + the indicator that appears when a website is loading)
* - none
* - left
* - right
* - top-left
* - top-right
*
* If you want to use guiset in your tridactylrc, you might want to use [[guiset_quiet]] instead.
*/
//#background
export async function guiset(rule: string, option: string) {
if (!(await guiset_quiet(rule, option))) {
throw new Error(":guiset failed. Please ensure native messenger is installed.")
}
return fillcmdline_tmp(3000, "userChrome.css written. Please restart Firefox to see the changes.")
}
/** @hidden */
//#background
export function cssparse(...css: string[]) {
console.log(CSS.parse(css.join(" ")))
}
/** @hidden */
//#background
export async function loadtheme(themename: string) {
if (!(await Native.nativegate("0.1.9"))) return
const separator = (await browserBg.runtime.getPlatformInfo()).os === "win" ? "\\" : "/"
// remove the "tridactylrc" bit so that we're left with the directory
const path = (await Native.getrcpath()).split(separator).slice(0, -1).join(separator) + separator + "themes" + separator + themename + ".css"
const file = await Native.read(path)
if (file.code !== 0) {
if (Object.keys(await config.get("customthemes")).includes(themename)) return
throw new Error("Couldn't read theme " + path)
}
return set("customthemes." + themename, file.content)
}
/** @hidden */
//#background
export async function unloadtheme(themename: string) {
return unset("customthemes." + themename)
}
/**
* Changes the current theme.
*
* If THEMENAME is any of the themes that can be found in the [Tridactyl repo](https://github.com/tridactyl/tridactyl/tree/master/src/static/themes) (e.g. 'dark'), the theme will be loaded from Tridactyl's internal storage.
*
* If THEMENAME is set to any other value except `--url`, Tridactyl will attempt to use its native binary (see [[native]]) in order to load a CSS file named THEMENAME from disk. The CSS file has to be in a directory named "themes" and this directory has to be in the same directory as your tridactylrc. If this fails, Tridactyl will attempt to load the theme from its internal storage.
*
* Lastly, themes can be loaded from URLs with `:colourscheme --url [url] [themename]`. They are stored internally - if you want to update the theme run the whole command again.
*
* Note that the theme name should NOT contain any dot.
*
* Example: `:colourscheme mysupertheme`
* On linux, this will load ~/.config/tridactyl/themes/mysupertheme.css
*
* __NB__: due to Tridactyl's architecture, the theme will take a small amount of time to apply as each page is loaded. If this annoys you, you may use [userContent.css](http://kb.mozillazine.org/index.php?title=UserContent.css&printable=yes) to make changes to Tridactyl earlier. For example, users using the dark theme may like to put
*
* ```
* :root {
* --tridactyl-bg: black !important;
* --tridactyl-fg: white !important;
* }
* ```
*
* in their `userContent.css`. Follow [issue #2510](https://github.com/tridactyl/tridactyl/issues/2510) if you would like to find out when we have made a more user-friendly solution.
*/
//#background
export async function colourscheme(...args: string[]) {
const themename = args[0] == "--url" ? args[2] : args[0]
// If this is a builtin theme, no need to bother with slow stuff
if (Metadata.staticThemes.includes(themename)) return set("theme", themename)
if (themename.search("\\.") >= 0) throw new Error(`Theme name should not contain any dots! (given name: ${themename}).`)
if (args[0] == "--url") {
if (themename === undefined) throw new Error(`You must provide a theme name!`)
let url = args[1]
if (url === "%") url = window.location.href // this is basically an easter egg
if (!(url.startsWith("http://") || url.startsWith("https://"))) url = "http://" + url
const css = await rc.fetchText(url)
set("customthemes." + themename, css)
} else {
await loadtheme(themename)
}
return set("theme", themename)
}
/**
* Write a setting to your user.js file. Requires a [[restart]] after running to take effect.
*
* @param key The key that should be set. Must not be quoted. Must not contain spaces.
* @param value The value the key should take. Quoted if a string, unquoted otherwise.
*
* Note that not all of the keys Firefox uses are suggested by Tridactyl.
*
* e.g.: `setpref general.warnOnAboutConfig false`
*/
//#background
export function setpref(key: string, ...value: string[]) {
return Native.writePref(key, value.join(" "))
}
/**
* Remove a setting from your user.js file.
*
* @param key The key that should be set. Must not be quoted. Must not contain spaces.
*
*/
//#background
export function removepref(key: string) {
return Native.removePref(key)
}
/**
* Like [[fixamo]] but quieter.
*
* Now purely a placebo as [[fixamo]] has been removed.
*/
//#background
export async function fixamo_quiet() {
return logger.warning("fixamo_quiet has been removed at the behest of the Firefox Security team. See :help fixamo for more info.")
}
/**
*
* Used to simply set
* ```js
* "privacy.resistFingerprinting.block_mozAddonManager":true
* "extensions.webextensions.restrictedDomains":""
* ```
* in about:config via user.js so that Tridactyl (and other extensions!) can be used on addons.mozilla.org and other sites.
*
* Removed at the request of the Firefox Security team. Replacements exist in our exemplar RC file.
*
* Requires `native` and a `restart`.
*/
//#background
export async function fixamo() {
fillcmdline_tmp(10000, "fixamo has been removed at the request of the Firefox Security team. Alternatives exist in our exemplar RC file.")
}
/**
* Uses the native messenger to open URLs.
*
* **Be *seriously* careful with this:**
*
* 1. the implementation basically execs `firefox --new-tab <your shell escaped string here>`
* 2. you can use it to open any URL you can open in the Firefox address bar,
* including ones that might cause side effects (firefox does not guarantee
* that about: pages ignore query strings).
*
* You've been warned.
*
* This uses the [[browser]] setting to know which binary to call. If you need to pass additional arguments to firefox (e.g. '--new-window'), make sure they appear before the url.
*/
//#background
export async function nativeopen(...args: string[]) {
const index = args.findIndex(arg => !arg.startsWith("-"))
let firefoxArgs = []
if (index >= 0) {
firefoxArgs = args.slice(0, index)
}
const url = args.slice(firefoxArgs.length).join(" ")
if (await Native.nativegate()) {
// First compute where the tab should be
const pos = await config.getAsync("tabopenpos")
let index = (await activeTab()).index + 1
switch (pos) {
case "last":
index = -1
break
case "related":
// How do we simulate that?
break
}
// Then make sure the tab is made active and moved to the right place
// when it is opened in the current window
const selecttab = tab => {
browser.tabs.onCreated.removeListener(selecttab)
tabSetActive(tab.id)
browser.tabs.move(tab.id, { index })
}
browser.tabs.onCreated.addListener(selecttab)
try {
if ((await browser.runtime.getPlatformInfo()).os === "mac") {
if ((await browser.windows.getCurrent()).incognito) {
throw new Error("nativeopen isn't supported in private mode on OSX. Consider installing Linux or Windows :).")
}
const osascriptArgs = ["-e 'on run argv'", "-e 'tell application \"Firefox\" to open location item 1 of argv'", "-e 'end run'"]
await Native.run("osascript " + osascriptArgs.join(" ") + " " + url)
} else {
const os = (await browser.runtime.getPlatformInfo()).os
if (firefoxArgs.length === 0) {
try {
const profile = await Native.getProfile()
if (profile.Name !== undefined) {
if (os === "win") {
firefoxArgs = [`-p "${profile.Name}"`]
} else {
firefoxArgs = [`-p '${profile.Name}'`]
}
} else if (profile.absolutePath !== undefined) {
if (os === "win") {
firefoxArgs = [`--profile "${profile.absolutePath}"`]
} else {
firefoxArgs = [`--profile '${profile.absolutePath}'`]
}
}
} catch (e) {
logger.debug(e)
firefoxArgs = []
}
firefoxArgs.push("--new-tab")
}
let escapedUrl: string
// We need to quote and escape single quotes in the
// url, otherwise an attacker could create an anchor with a url
// like 'file:// && $(touch /tmp/dead)' and achieve remote code
// execution when the user tries to follow it with `hint -W tabopen`
if (os === "win") {
escapedUrl = escape.windows_cmd(url)
} else {
escapedUrl = escape.sh(url)
}
await Native.run(`${config.get("browser")} ${firefoxArgs.join(" ")} ${escapedUrl}`)
}
setTimeout(() => browser.tabs.onCreated.removeListener(selecttab), 100)
} catch (e) {
browser.tabs.onCreated.removeListener(selecttab)
throw e
}
}
}
/**
* Run command in /bin/sh (unless you're on Windows), and print the output in the command line. Non-zero exit codes and stderr are ignored, currently.
*
* Requires the native messenger, obviously.
*
* If you're using `exclaim` with arguments coming from a pipe, consider using [[shellescape]] to properly escape arguments and to prevent unsafe commands.
*
* If you want to use a different shell, just prepend your command with whatever the invocation is and keep in mind that most shells require quotes around the command to be executed, e.g. `:exclaim xonsh -c "1+2"`.
*
* Aliased to `!` but the exclamation mark **must be followed with a space**.
*/
//#background
export async function exclaim(...str: string[]) {
let done = Promise.resolve()
if (await Native.nativegate()) {
done = fillcmdline((await Native.run(str.join(" "))).content)
}
return done
} // should consider how to give option to fillcmdline or not. We need flags.
/**
* Like exclaim, but without any output to the command line.
*/
//#background
export async function exclaim_quiet(...str: string[]) {
let result = ""
if (await Native.nativegate()) {
result = (await Native.run(str.join(" "))).content
}
return result
}
/**
* Tells you if the native messenger is installed and its version.
*
* For snap, flatpak, and other sandboxed installations, additional setup is required – see https://github.com/tridactyl/tridactyl#extra-features-through-native-messaging.
*/
//#background
export async function native() {
const version = await Native.getNativeMessengerVersion(true)
let done
if (version !== undefined) {
done = fillcmdline("# Native messenger is correctly installed, version " + version)
} else {
done = fillcmdline("# Native messenger not found. Please run `:nativeinstall` and follow the instructions.")
}
return done
}
/**
* Copies the installation command for the native messenger to the clipboard and asks the user to run it in their shell.
*
* The native messenger's source code may be found here: https://github.com/tridactyl/native_messenger/blob/master/src/native_main.nim
*
* If your corporate IT policy disallows execution of binaries which have not been whitelisted but allows Python scripts, you may instead use the old native messenger by running `install.sh` or `win_install.ps1` from https://github.com/tridactyl/tridactyl/tree/master/native - the main downside is that it is significantly slower.
*
* For snap, flatpak, and other sandboxed installations, additional setup is required – see https://github.com/tridactyl/tridactyl#extra-features-through-native-messaging.
*/
//#background
export async function nativeinstall() {
const tag = TRI_VERSION.includes("pre") ? "master" : TRI_VERSION
let done
const installstr = (await config.get("nativeinstallcmd")).replace("%TAG", tag)
await yank(installstr)
if ((await browser.runtime.getPlatformInfo()).os === "win") {
done = fillcmdline("# Installation command copied to clipboard. Please paste and run it in cmd.exe (other shells won't work) to install the native messenger.")
} else {
done = fillcmdline("# Installation command copied to clipboard. Please paste and run it in your shell to install the native messenger.")
}
return done
}
/** Writes current config to a file.
NB: an RC file is not required for your settings to persist: all settings are stored in a local Firefox storage database by default as soon as you set them.
With no arguments supplied the excmd will try to find an appropriate
config path and write the rc file to there. Any argument given to the
excmd excluding the `-f` flag will be treated as a path to write the rc
file to relative to the native messenger's location (`~/.local/share/tridactyl/`). By default, it silently refuses to overwrite existing files.
The RC file will be split into sections that will be created if a config
property is discovered within one of them:
- General settings
- Binds
- Aliases
- Autocmds
- Autocontainers
- Logging
Note:
- Subconfig paths fall back to using `js tri.config.set(key: obj)` notation.
- This method is also used as a fallback mechanism for objects that didn't hit
any of the heuristics.
Available flags:
- `-f` will overwrite the config file if it exists.
- `--clipboard` write config to clipboard - no [[native]] required
@param args an optional string of arguments to be parsed.
@returns the parsed config.
*/
//#background
export async function mktridactylrc(...args: string[]) {
let overwrite = false
const argParse = (args: string[]): string[] => {
if (args[0] === "-f") {
overwrite = true
args.shift()
argParse(args)
}
return args
}
const file = argParse(args).join(" ") || undefined
const conf = config.parseConfig()
if (file == "--clipboard") {
setclip(conf)
return fillcmdline_tmp(3000, "# RC copied to clipboard")
}
if ((await Native.nativegate("0.1.11")) && !(await rc.writeRc(conf, overwrite, file))) logger.error("Could not write RC file")
return conf
}
/**
* Runs an RC file from disk or a URL
*
* This function accepts flags: `--url`, `--clipboard` or `--strings`.
*
* If no argument given, it will try to open ~/.tridactylrc, ~/.config/tridactyl/tridactylrc or $XDG_CONFIG_HOME/tridactyl/tridactylrc in reverse order. You may use a `_` in place of a leading `.` if you wish, e.g, if you use Windows.
*
* On Windows, the `~` expands to `%USERPROFILE%`.
*
* The `--url` flag will load the RC from the URL. If no url is specified with the `--url` flag, the current page's URL is used to locate the RC file. Ensure the URL you pass (or page you are on) is a "raw" RC file, e.g. https://raw.githubusercontent.com/tridactyl/tridactyl/master/.tridactylrc and not https://github.com/tridactyl/tridactyl/blob/master/.tridactylrc.
*
* Tridactyl won't run on many raw pages due to a Firefox bug with Content Security Policy, so you may need to use the `source --url [URL]` form.
*
* The `--clipboard` flag will load the RC from the clipboard, which is useful for people cannot install the native messenger or do not wish to store their RC online. You can use this with `mktridactylrc --clipboard`.
*
* The `--strings` flag will load the RC from rest arguments. It could be useful if you want to execute a batch of commands in js context. Eg: `js tri.excmds.source("--strings", [cmd1, cmd2].join("\n"))`.
*
* The RC file is just a bunch of Tridactyl excmds (i.e, the stuff on this help page). Settings persist in local storage. There's an [example file](https://raw.githubusercontent.com/tridactyl/tridactyl/master/.tridactylrc) if you want it.
*
* There is a [bug](https://github.com/tridactyl/tridactyl/issues/1409) where not all lines of the RC file are executed if you use `sanitise` at the top of it. We instead recommend you put `:bind ZZ composite sanitise tridactyllocal; qall` in your RC file and use `ZZ` to exit Firefox.
*
* @param args the file/URL to open. For files: must be an absolute path, but can contain environment variables and things like ~.
*/
//#background
export async function source(...args: string[]) {
if (args[0] === "--url") {
let url = args[1]
if (!url || url === "%") url = window.location.href
if (!new RegExp("^(https?://)|data:").test(url)) url = "http://" + url
await rc.sourceFromUrl(url)
} else if (args[0] === "--strings") {
await rc.runRc(args.slice(1).join(" "))
} else if (args[0] === "--clipboard") {
const text = await getclip()
await rc.runRc(text)
} else {
const file = args.join(" ") || undefined
if ((await Native.nativegate("0.1.3")) && !(await rc.source(file))) {
logger.error("Could not find RC file")
}
}
}
/**
* Same as [[source]] but suppresses all errors
*/
//#background
export async function source_quiet(...args: string[]) {
try {
await source(...args)
} catch (e) {
logger.info("Automatic loading of RC file failed.")
}
}
/**
* Updates the native messenger if it is installed, using our GitHub repo. This is run every time Tridactyl is updated.
*
* If you want to disable this, or point it to your own native messenger, edit the `nativeinstallcmd` setting.
*/
//#background
export async function updatenative(interactive = true) {
if (!(await Native.nativegate("0", interactive))) {
return
} else if ((await browser.runtime.getPlatformInfo()).os === "mac") {
if (interactive) logger.error("Updating the native messenger on OSX is broken. Please use `:nativeinstall` instead.")
return
}
const tag = TRI_VERSION.includes("pre") ? "master" : TRI_VERSION
const update_command = (await config.get("nativeinstallcmd")).replace("%TAG", tag)
const native_version = await Native.getNativeMessengerVersion()
if (semverCompare(native_version, "0.2.0") < 0) {
await Native.run(update_command)
} else if (semverCompare(native_version, "0.3.1") < 0) {
if (interactive) {
throw new Error("Updating is broken on this version of the native messenger. Please use `:nativeinstall` instead.")
}
return
} else {
await Native.runAsync(update_command)
}
if (interactive) native()
}
/**
* Restarts firefox with the same commandline arguments.
*
* Warning: This can kill your tabs, especially if you :restart several times
* in a row
*/
//#background
export async function restart() {
const profiledir = await Native.getProfileDir()
const browsercmd = await config.get("browser")
if ((await browser.runtime.getPlatformInfo()).os === "win") {
const reply = await Native.winFirefoxRestart(profiledir, browsercmd)
logger.info("[+] win_firefox_restart 'reply' = " + JSON.stringify(reply))
if (Number(reply.code) === 0) {
fillcmdline("#" + reply.content)
qall()
} else {
fillcmdline("#" + reply.error)
}
} else {
const firefox = (await Native.ff_cmdline()).join(" ")
// Wait for the lock to disappear, then wait a bit more, then start firefox
Native.run(`while readlink ${profiledir}/lock ; do sleep 1 ; done ; sleep 1 ; ${firefox}`)
qall()
}
}
/** Download the current document.
*
* If you have the native messenger v>=0.1.9 installed, the function accepts an optional argument, filename, which can be:
* - An absolute path
* - A path starting with ~, which will be expanded to your home directory
* - A relative path, relative to the native messenger executable (e.g. ~/.local/share/tridactyl on linux).
* If filename is not given, a download dialogue will be opened. If filename is a directory, the file will be saved inside of it, its name being inferred from the URL. If the directories mentioned in the path do not exist or if a file already exists at this path, the file will be kept in your downloads folder and an error message will be given.
*
* **NB**: if a non-default save location is chosen, Firefox's download manager will say the file is missing. It is not - it is where you asked it to be saved.
*
* Flags:
* - `--overwrite`: overwrite the destination file.
* - `--cleanup`: removes the downloaded source file e.g. `$HOME/Downlods/downloaded.doc` if moving it to the desired directory fails.
*/
//#content
export async function saveas(...args: string[]) {
let overwrite = false
let cleanup = false
const argParse = (args: string[]): string[] => {
if (args[0] === "--overwrite") {
overwrite = true
args.shift()
argParse(args)
}
if (args[0] === "--cleanup") {
cleanup = true
args.shift()
argParse(args)
}
return args
}
const file = argParse(args).join(" ") || undefined
const requiredNativeMessengerVersion = "0.3.2"
if ((overwrite || cleanup) && !(await Native.nativegate(requiredNativeMessengerVersion, false))) {
throw new Error(`":saveas --{overwrite, cleanup}" requires native ${requiredNativeMessengerVersion} or later`)
}
if (args.length > 0) {
const filename = await Messaging.message("download_background", "downloadUrlAs", window.location.href, file, overwrite, cleanup)
return fillcmdline_tmp(10000, `Download completed: ${filename} stored in ${file}`)
} else {
return Messaging.message("download_background", "downloadUrl", window.location.href, true)
}
}
// }}}
/** @hidden */
//#background_helper
function tabSetActive(id: number) {
return browser.tabs.update(id, { active: true })
}
// }}}
// {{{ PAGE CONTEXT
/** @hidden */
//#content_helper
let JUMPED: boolean
/** @hidden */
//#content_helper
let JUMP_TIMEOUTID: number | undefined