Compare commits

...

721 commits

Author SHA1 Message Date
Oliver Blanthorn aa225538ed
release 1.25.1
Some checks failed
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
e2e / test (push) Has been cancelled
lint / commit-emails (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
# Release 1.25.1 / 2026-09-08

- New features

    - `:tabmove [amount] [index]` added for use primarily in ex-mode binds ([#5045](https://github.com/tridactyl/tridactyl/issues/5045))

- Bug fixes

    - `:mode [custom]` now works again ([#5518](https://github.com/tridactyl/tridactyl/issues/5518))
    - `:mkt` is now noisier about paths and failure
    - `:set logging.` works again ([#5513](https://github.com/tridactyl/tridactyl/issues/5513))
    - `searchurls` can now have `%s` in their subdomains ([#5523](https://github.com/tridactyl/tridactyl/issues/5523))

- Miscellaneous

    - noreply email addresses are henceforth banned from commits ([#5517](https://github.com/tridactyl/tridactyl/issues/5517))
    - flag documentation formalised to preserve metadata for eventual reuse ([#5511](https://github.com/tridactyl/tridactyl/issues/5511))

Thanks to all of our contributors for this release: Oliver Blanthorn, Robert Hill, dependabot[bot] and Marc González Moratona.

Extra special thanks go to Marc González Moratona and Robert Hill who both contributed for the first time.

Last, but not least - thank you to everyone who reported the issues we closed in this release: bovine3dom, pennae, stevium, syntaxenjoyer and veivataan.
2026-09-08 14:37:54 +02:00
Oliver Blanthorn 697b73dab9
Update changelog for 1.25.1 2026-09-08 13:26:47 +02:00
Oliver Blanthorn 066b046285
Update changelog for 1.25.1 2026-09-08 13:26:18 +02:00
Oliver Blanthorn 11693da62e
Fix broken CI in FF155+
Some checks are pending
e2e / test (push) Waiting to run
lint / lint (mozilla) (push) Waiting to run
lint / lint (unit) (push) Waiting to run
lint / commit-emails (push) Waiting to run
lint / lint (lint) (push) Waiting to run
Website / build (push) Waiting to run
Website / deploy (push) Blocked by required conditions
Flag needed to use Selenium ExecuteScript on WebExtension pages
2026-09-07 17:11:58 +02:00
Oliver Blanthorn c2e6452a46
Fix #5523: allow %s replacement in subdomains 2026-09-07 16:36:42 +02:00
Oliver Blanthorn c330f7eebd
Update changelog for 1.25.1
Some checks are pending
e2e / test (push) Waiting to run
lint / lint (mozilla) (push) Waiting to run
lint / lint (unit) (push) Waiting to run
lint / commit-emails (push) Waiting to run
lint / lint (lint) (push) Waiting to run
Website / build (push) Waiting to run
Website / deploy (push) Blocked by required conditions
2026-09-06 12:49:25 +02:00
Oliver Blanthorn ef99ffb858
Merge pull request #5516 from MarcGonzalezMoratona/tabmove-index
Some checks failed
e2e / test (push) Has been cancelled
lint / commit-emails (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Fix #5045: add optional tab argument to :tabmove
2026-09-02 11:09:25 +00:00
Oliver Blanthorn e4793a3284
Merge pull request #5521 from tridactyl/dependabot/npm_and_yarn/nanoid-3.3.18
Bump nanoid from 3.3.8 to 3.3.18
2026-09-02 10:16:26 +00:00
Oliver Blanthorn f6173ecb3e
Merge pull request #5520 from tridactyl/dependabot/npm_and_yarn/browserslist-4.28.8
Bump browserslist from 4.28.6 to 4.28.8
2026-09-02 10:16:12 +00:00
Marc González Moratona f1ceadc2d0
🤖 Fix #5045: add optional tab argument to :tabmove
tabmove now accepts a second, optional argument selecting which tab to
move (resolved via tabFromIndex), defaulting to the current tab. This
makes `ex.execute_ex_on_completion_args tabmove -1` operate on the tab
selected in the buffer list instead of always the active one.
2026-09-02 12:12:50 +02:00
dependabot[bot] 1a186c97d2
Bump nanoid from 3.3.8 to 3.3.18
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.8 to 3.3.18.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/3.3.18/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.8...3.3.18)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-version: 3.3.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 10:08:20 +00:00
dependabot[bot] ae8eb44146
Bump browserslist from 4.28.6 to 4.28.8
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.6 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.6...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 10:08:12 +00:00
Oliver Blanthorn d806a7b83e
Merge pull request #5519 from tridactyl/fix_5517
Fix #5517: update docs + linter to discourage `noreply` addresses
2026-09-02 10:06:37 +00:00
Oliver Blanthorn 7d0a3ca1d9
Fix #5517: update docs + linter to discourage noreply addresses 2026-09-02 12:03:36 +02:00
Oliver Blanthorn d30541a2ff
Fix #5518: allow custom modes again
Some checks failed
e2e / test (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
2026-08-31 19:00:06 +00:00
Oliver Blanthorn 5020c4c46f
Make :mkt noisier about paths and failure
Some checks failed
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
e2e / test (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
2026-08-30 13:12:45 +00:00
Oliver Blanthorn e7caf837ae
Fix #5513: preserve type information about internal objects 2026-08-30 12:23:47 +02:00
Oliver Blanthorn 8a6bae175e
Merge pull request #5511 from uhs-robert/feat/arg-metadata
Some checks failed
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
e2e / test (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Add @arg tag support for documenting excmd flags
2026-08-28 18:40:42 +00:00
Oliver Blanthorn a0073eaf01
Replace @endflags with blank newline detection
This is kind of a hack but I won't lose too much sleep over it
2026-08-28 14:37:38 +02:00
Oliver Blanthorn 5e1929db02
Use tsdoc.json for our own doc tags 2026-08-28 14:37:22 +02:00
Oliver Blanthorn 341aa6621d
release 1.25.0
# Release 1.25.0 / 2026-08-28

Codename "Carpenter"

- New features

    - `:bind` can now distinguish keydown and keyup events with `<D-...>` and `<U-...>` ([#1645](https://github.com/tridactyl/tridactyl/issues/1645))
    - `:smoothscrollwizard` sets up true smooth scrolling key bindings ([#2170](https://github.com/tridactyl/tridactyl/issues/2170))
    - `:tabclose --match` can now close multiple tabs with literal, case-sensitive title or URL matches ([#4777](https://github.com/tridactyl/tridactyl/issues/4777))
    - `:find -r` now supports JavaScript regular expressions ([#1612](https://github.com/tridactyl/tridactyl/issues/1612))
    - `:find` has completions once more
    - `gr` is now bound to newly sandboxed `:reader` where our binds work ([#5453](https://github.com/tridactyl/tridactyl/issues/5453))
    - `modeindicatorshowlastex` can now show the Ex command that `:repeat` would execute in the mode indicator ([#1234](https://github.com/tridactyl/tridactyl/issues/1234))
    - `gI` lets you choose an input using hints ([#3209](https://github.com/tridactyl/tridactyl/issues/3209))
    - `#` in visual mode now yanks a link to the selected text fragment ([#5407](https://github.com/tridactyl/tridactyl/issues/5407))
    - `<C-o>x` executes the current ex-command on all completions ([#3133](https://github.com/tridactyl/tridactyl/issues/3133))
    - `:fillcmdline --wait` waits for the command line to close before returning for use in `:composite` ([#5423](https://github.com/tridactyl/tridactyl/issues/5423))
    - `:help` now falls back to `:apropos` matches when prefix matching finds nothing ([#5478](https://github.com/tridactyl/tridactyl/issues/5478))
    - `:abbreviate` expands command-line abbreviations as you type ([#675](https://github.com/tridactyl/tridactyl/issues/675))
    - `:define` now looks up glossary terms, which are also searchable via `:help` and `:apropos` ([#2980](https://github.com/tridactyl/tridactyl/issues/2980))
    - `:set historysort frequency | recent | frecency` lets you control `:open` completions ([#2276](https://github.com/tridactyl/tridactyl/issues/2276))
    - `repeatblacklist` lets you exclude ex commands from `:repeat` ([#4159](https://github.com/tridactyl/tridactyl/issues/4159))
    - `:set` completions now find settings containing the query when no names start with it ([#1175](https://github.com/tridactyl/tridactyl/issues/1175))
    - `<PageUp>` and `<PageDown>` now page (ish) through ex completions ([#4164](https://github.com/tridactyl/tridactyl/issues/4164))
    - `ga` now switches to the tab that most recently stopped playing audio when none is audible ([#5049](https://github.com/tridactyl/tridactyl/issues/5049))
    - `:tabnext --skip-discarded` and `:tabprev --skip-discarded` skip discarded tabs ([#5246](https://github.com/tridactyl/tridactyl/issues/5246))
    - `:findrc` displays the local RC file selected by automatic RC discovery ([#3806](https://github.com/tridactyl/tridactyl/issues/3806))
    - `DocFocus` and `DocBlur` autocmds can run commands when pages gain or lose focus ([#1109](https://github.com/tridactyl/tridactyl/issues/1109))
    - `:recontain`, `:containerclose`, `:containerdelete`, and `:containerupdate` now get completions ([#5066](https://github.com/tridactyl/tridactyl/issues/5066))
    - `:containercreate --or-update` creates missing containers or updates existing ones ([#3025](https://github.com/tridactyl/tridactyl/issues/3025))
    - `]]` and `[[` now recognise next/previous links in more languages ([#324](https://github.com/tridactyl/tridactyl/issues/324))
    - `:set themeprivacy true` hides the active theme from page root elements ([#1586](https://github.com/tridactyl/tridactyl/issues/1586))
    - `:find -s` and `:find -i` now override `findcase` for case-sensitive and case-insensitive searches ([#2286](https://github.com/tridactyl/tridactyl/issues/2286))
    - `ModeEnter` and `ModeLeave` autocmds run when Tridactyl changes mode ([#997](https://github.com/tridactyl/tridactyl/issues/997))
    - `:autocontaindelete` removes individual `autocontain` rules ([#3649](https://github.com/tridactyl/tridactyl/issues/3649))
    - `<C-o>` in insert mode now executes a single normal mode binding ([#2370](https://github.com/tridactyl/tridactyl/issues/2370))
    - ex-mode completions now fall back to documentation search for unknown commands ([#944](https://github.com/tridactyl/tridactyl/issues/944))
    - `:quickmarkremove` removes a quickmark and its associated `gn`, `go`, `gw`, and `gp` bindings ([#4724](https://github.com/tridactyl/tridactyl/issues/4724))
    - `:set modeindicatorshowkeys` now shows Vimperator hint filters in the mode indicator ([#1669](https://github.com/tridactyl/tridactyl/issues/1669))
    - `:sanitise tridactylconfig` clears stored user configuration without removing commandline history or custom themes ([#5472](https://github.com/tridactyl/tridactyl/issues/5472))
    - `:unkeymap` removes key translations set with `:keymap` ([#1169](https://github.com/tridactyl/tridactyl/issues/1169))
    - `completions.History.autoselect` can now select the first history completion automatically ([#1370](https://github.com/tridactyl/tridactyl/issues/1370))
    - `:set searchurlopen` controls whether searchurls appear in `:open`, `:tabopen` and `:winopen` completions ([#4741](https://github.com/tridactyl/tridactyl/issues/4741))
    - `:set bmarkopen` can now exclude bookmarks from `:open`, `:tabopen`, and `:winopen` completions ([#1817](https://github.com/tridactyl/tridactyl/issues/1817))
    - `:urlmodify -s` now supports setting multiple query parameters ([#3361](https://github.com/tridactyl/tridactyl/issues/3361))
    - `:reader` now shows the word count and estimated reading time ([#4681](https://github.com/tridactyl/tridactyl/issues/4681))
    - `:tgroupswitch` now autoselects matching tab groups ([#4649](https://github.com/tridactyl/tridactyl/issues/4649))
    - `:set completions.Tab.initialposition top|auto|active` added to control where `:tab` completions open ([#4177](https://github.com/tridactyl/tridactyl/issues/4177))
    - `<ArrowUp>` and `<ArrowDown>` navigate completions when one is selected ([#3732](https://github.com/tridactyl/tridactyl/issues/3732))
    - `:tabdetach` now accepts `#/%` magic indices
    - `:find` uses much less washed-out native CSS highlights in Firefox 140+
    - Commands starting with a space are no longer saved to command history ([#4763](https://github.com/tridactyl/tridactyl/issues/4763))
    - `:js -rc` caches JavaScript source until the page or background context unloads ([#3839](https://github.com/tridactyl/tridactyl/issues/3839))
    - `:set hinthideobscured true` hides hints for elements obscured by other elements ([#4746](https://github.com/tridactyl/tridactyl/issues/4746))
    - `countaware` can now enable or disable counts per URL or mode with `:seturl` and `:setmode` ([#4993](https://github.com/tridactyl/tridactyl/issues/4993))
    - `:urlmodify --safe` prevents repeated navigation to the same URL within one second, avoiding redirect loops in `:autocmd`s ([#516](https://github.com/tridactyl/tridactyl/issues/516))
    - `:set hintnames words` uses random short words for hints ([#5482](https://github.com/tridactyl/tridactyl/issues/5482))
    - `superignore` can now be toggled globally from the toolbar popup ([#822](https://github.com/tridactyl/tridactyl/issues/822))
    - `:colourscheme --url` reuses downloaded themes unless given `--update` to ease use in RC files
    - `:ttscontrol` now supports `play`, `pause`, `playpause`, and `stop` ([#217](https://github.com/tridactyl/tridactyl/issues/217))
    - `:downloads` now opens `about:downloads` in a new tab ([#615](https://github.com/tridactyl/tridactyl/issues/615))
    - `:dialog` opens Firefox `about:` pages, with `:d` as an alias ([#137](https://github.com/tridactyl/tridactyl/issues/137))
    - unambiguous prefixes of ex-commands are now accepted as ex-commands ([#3416](https://github.com/tridactyl/tridactyl/issues/3416))
    - `:guiset sidebar hide` hides the Firefox sidebar ([#5232](https://github.com/tridactyl/tridactyl/issues/5232))
    - `:unbind --all` unbinds every key in a mode, while `--mode=*` targets every mode ([#1774](https://github.com/tridactyl/tridactyl/issues/1774))
    - `text.unix_word_rubout` deletes back to the previous sequence of letters ([#3407](https://github.com/tridactyl/tridactyl/issues/3407))
    - `Tab` now highlights the selected hint label as well as the element ([#3751](https://github.com/tridactyl/tridactyl/issues/3751))
    - `,i` in the exemplar `.tridactylrc` opens Tridactyl GitHub issues by number

- Bug fixes

    - `:undo` should never randomly restore an older window rather than your most recently closed tab ([#772](https://github.com/tridactyl/tridactyl/issues/772))
    - `blacklistkeys` now supports keyseq syntax such as `<C-,>`; `:bind --mode=browser` binds are protected automatically ([#2712](https://github.com/tridactyl/tridactyl/issues/2712))
    - `leavegithubalone` now applies in all modes except ignore mode ([#2082](https://github.com/tridactyl/tridactyl/issues/2082))
    - `sanitise` no longer causes lines in RC files to be skipped ([#1409](https://github.com/tridactyl/tridactyl/issues/1409))
    - hints now appear above newfangled modal dialogs ([#4954](https://github.com/tridactyl/tridactyl/issues/4954))
    - `noiframe` now defaults to `lazy`, fixing a few pathological websites ([#639](https://github.com/tridactyl/tridactyl/issues/639))
    - `:jsonview` works again in FF152+
    - `:tab` and `:taball` no longer move the command line up when closing tabs ([#3174](https://github.com/tridactyl/tridactyl/issues/3174))
    - commands using `fillcmdline_tmp` no longer wait for its temporary message to disappear ([#5422](https://github.com/tridactyl/tridactyl/issues/5422))
    - `w` no longer collapses selections in visual mode ([#2100](https://github.com/tridactyl/tridactyl/issues/2100))
    - `:help` once again shows the full ex-command for related binds ([#1352](https://github.com/tridactyl/tridactyl/issues/1352))
    - `g;` now skips closed tabs ([#3506](https://github.com/tridactyl/tridactyl/issues/3506))
    - `gi` now remembers the last input across page loads ([#3124](https://github.com/tridactyl/tridactyl/issues/3124))
    - `:taball` now respects `:set tabsort mru` ([#4668](https://github.com/tridactyl/tridactyl/issues/4668))
    - JS hints now include elements with `onclick` handlers ([#5364](https://github.com/tridactyl/tridactyl/issues/5364))
    - tab changes are now batched and sent only to the active tab, reducing memory use when restoring large sessions ([#3973](https://github.com/tridactyl/tridactyl/issues/3973))
    - JS hints should no longer cause tabs to leak memory on pages with lots of dynamic content ([#5230](https://github.com/tridactyl/tridactyl/issues/5230))
    - mode indicator now updates correctly when focus changes ([#414](https://github.com/tridactyl/tridactyl/issues/414))
    - tab completions no longer accumulate listeners and objects, reducing memory usage ([#1302](https://github.com/tridactyl/tridactyl/issues/1302))
    - binds now work in same-origin iframes, including dynamically added frames (mostly Google Docs) ([#1971](https://github.com/tridactyl/tridactyl/issues/1971))
    - visual mode now works in same-origin iframes
    - scroll positions now restore correctly when going back or forward in history ([#834](https://github.com/tridactyl/tridactyl/issues/834))
    - `:keyfeed` can now be used in keybinds ([#5403](https://github.com/tridactyl/tridactyl/issues/5403))
    - `zz` now resets page zoom to Firefox's default zoom level ([#3581](https://github.com/tridactyl/tridactyl/issues/3581))
    - `:colourscheme --url` now infers the theme name from the URL filename ([#3289](https://github.com/tridactyl/tridactyl/issues/3289))
    - numeric `vimperator-reflow` hints no longer clash after filtering ([#5279](https://github.com/tridactyl/tridactyl/issues/5279))
    - `:find` highlights now stay aligned when pages resize, scroll, or update dynamically ([#2666](https://github.com/tridactyl/tridactyl/issues/2666))
    - `:tabfirst` and `:tablast` now always use Firefox's tab order, regardless of `tabsort` ([#4714](https://github.com/tridactyl/tridactyl/issues/4714))
    - `:unseturl` now completes URL patterns ([#3862](https://github.com/tridactyl/tridactyl/issues/3862))
    - `:guiset` now reports the profile path where it wrote `userChrome.css` ([#3064](https://github.com/tridactyl/tridactyl/issues/3064))
    - JS hints should no longer leak memory when pages replace elements ([#5425](https://github.com/tridactyl/tridactyl/issues/5425))
    - `;p` now filters out empty, whitespace-only and redundant text hints ([#5178](https://github.com/tridactyl/tridactyl/issues/5178))
    - `:tabdetach` now errors instead of moving a private tab into a non-private window when detaching fails
    - `:nmode` now ignores an initial keyup by default (`:nmode --strict` accepts it) ([#5475](https://github.com/tridactyl/tridactyl/issues/5475))
    - `:setpref` completions work again on Windows ([#1103](https://github.com/tridactyl/tridactyl/issues/1103))
    - `text.beginning_of_line` works again on inputs with selection-handling scripts ([#3890](https://github.com/tridactyl/tridactyl/issues/3890))
    - `:help` setting inputs now apply changes when pressing Enter ([#3129](https://github.com/tridactyl/tridactyl/issues/3129))
    - command completions now work correctly with aliases ([#5464](https://github.com/tridactyl/tridactyl/issues/5464))
    - `autocontain` now follows the configured proxy order, including failover proxies ([#4917](https://github.com/tridactyl/tridactyl/issues/4917))
    - `:tabdiscard --all` now discards all tabs even when a tab title contains 'all' (lol) ([#5147](https://github.com/tridactyl/tridactyl/issues/5147))
    - non-ASCII URLs now display correctly in completion panes and copy correctly ([#4519](https://github.com/tridactyl/tridactyl/issues/4519))
    - `:tabmove +1` and `:tabmove -1` now work correctly with hidden and pinned tabs ([#5441](https://github.com/tridactyl/tridactyl/issues/5441))
    - configuration changes no longer get lost when made in quick succession ([#5431](https://github.com/tridactyl/tridactyl/issues/5431))
    - `:hint` no longer shows hints for hidden or off-screen elements ([#5434](https://github.com/tridactyl/tridactyl/issues/5434))
    - Page keypresses should no longer be swallowed after cancelling a keybinding ([#4958](https://github.com/tridactyl/tridactyl/issues/4958))
    - `:autocontain` no longer loops when multiple containers share a name ([#2293](https://github.com/tridactyl/tridactyl/issues/2293))
    - Command-line messages and update checks no longer replace the command used by `:repeat`
    - `:editor` now reports errors instead of silently failing, including when no editable element was previously focused ([#4697](https://github.com/tridactyl/tridactyl/issues/4697))
    - `insert` and `input` modes no longer interpret leading numbers as numeric prefixes ([#4492](https://github.com/tridactyl/tridactyl/issues/4492))
    - `<C-u>` now works in multiline contenteditable elements ([#1151](https://github.com/tridactyl/tridactyl/issues/1151))
    - `<Space>` in the command line no longer inserts spaces out of order when input arrives quickly ([#5447](https://github.com/tridactyl/tridactyl/issues/5447))
    - `f` hint mode now ignores disabled form controls
    - `:urlincrement` preserves percent encoding ([#4723](https://github.com/tridactyl/tridactyl/issues/4723))
    - `:bind` now reports binds for the focused window's URL ([#5092](https://github.com/tridactyl/tridactyl/issues/5092))
    - site-specific settings now update correctly when switching between Firefox windows ([#5443](https://github.com/tridactyl/tridactyl/issues/5443))
    - `:autocontain` no longer creates duplicate containers when multiple matching tabs open at once ([#5290](https://github.com/tridactyl/tridactyl/issues/5290))
    - config migration now runs immediately, so settings changed during startup are no longer overwritten ([#5449](https://github.com/tridactyl/tridactyl/issues/5449))
    - `:mode` now reports an error instead of breaking Tridactyl when called without a mode ([#5388](https://github.com/tridactyl/tridactyl/issues/5388))
    - `:tabdetach` works again when Firefox cannot resolve the default new-window URI ([#5454](https://github.com/tridactyl/tridactyl/issues/5454))
    - `:tabopen` now matches autocontainer rules against canonicalised URLs
    - `:editor` now works with textareas that require input events to accept changes ([#5359](https://github.com/tridactyl/tridactyl/issues/5359))
    - insert mode now works for input elements in closed shadow DOMs ([#5412](https://github.com/tridactyl/tridactyl/issues/5412))
    - `:unbind` can now unbind binds inherited from other modes ([#5389](https://github.com/tridactyl/tridactyl/issues/5389))
    - `:reader` no longer overrides syntax highlighting in code blocks ([#4931](https://github.com/tridactyl/tridactyl/issues/4931))
    - `\\` in RC files now preserves a literal `\` ([#2334](https://github.com/tridactyl/tridactyl/issues/2334))
    - `:nmode` works when called from insert mode ([#4437](https://github.com/tridactyl/tridactyl/issues/4437))
    - `:open` and `:tabopen` now work with `file:` URLs from `searchurls` ([#1415](https://github.com/tridactyl/tridactyl/issues/1415))
    - expired `:reader` tabs now explain how to return with `:back` and run `:reader` again ([#5320](https://github.com/tridactyl/tridactyl/issues/5320))
    - `:set hintshift true` now automatically highlights the shifted hint ([#2418](https://github.com/tridactyl/tridactyl/issues/2418))
    - `:bmarks -b` now shows bookmark completions ([#4166](https://github.com/tridactyl/tridactyl/issues/4166))
    - hints no longer remain on the page after leaving `hint` mode, no matter how you left ([#1236](https://github.com/tridactyl/tridactyl/issues/1236))
    - more non-standard dropdowns now enter insert mode when focused ([#281](https://github.com/tridactyl/tridactyl/issues/281))
    - Vimperator hint filtering now matches spaces literally when Space is unbound ([#3029](https://github.com/tridactyl/tridactyl/issues/3029))
    - `:taball`, `:tabgrab`, and window completions now update when tabs change ([#2645](https://github.com/tridactyl/tridactyl/issues/2645))
    - custom themes no longer require a `tridactylrc` file ([#1378](https://github.com/tridactyl/tridactyl/issues/1378))
    - `:colourscheme --url` now more reliably applies newly downloaded custom themes ([#5307](https://github.com/tridactyl/tridactyl/issues/5307))
    - `gg` and `G` now scroll smoothly when `:set smoothscroll true` is enabled ([#3639](https://github.com/tridactyl/tridactyl/issues/3639))
    - `:gobble` now accepts `Tab` as part of the key sequence
    - `:bindwizard` now includes `<Tab>` in the generated key sequence ([#4772](https://github.com/tridactyl/tridactyl/issues/4772))
    - `:set modeindicatormodes.normal false` now hides the mode indicator immediately on page load ([#5085](https://github.com/tridactyl/tridactyl/issues/5085))
    - `:setnull` now offers setting completions ([#3673](https://github.com/tridactyl/tridactyl/issues/3673))
    - `<S-Space>` bindings now work ([#4175](https://github.com/tridactyl/tridactyl/issues/4175))
    - Vimperator hints now prefer exact matches over longer hints with the same prefix ([#2935](https://github.com/tridactyl/tridactyl/issues/2935))
    - `:source --url` no longer shows filesystem completions ([#2331](https://github.com/tridactyl/tridactyl/issues/2331))
    - `<C-o>w` now works with `:bmarks ` completions ([#5451](https://github.com/tridactyl/tridactyl/issues/5451))
    - scrolling works better on pages using CSS smooth scrolling ([#5452](https://github.com/tridactyl/tridactyl/issues/5452))
    - `:winopen -popup -c` now opens popup windows in the specified container ([#4959](https://github.com/tridactyl/tridactyl/issues/4959))
    - `fillcmdline` bindings now buffer page keypresses reliably, including aliases and `:composite` commands ([#5436](https://github.com/tridactyl/tridactyl/issues/5436))
    - `:hint` now reports invalid CSS selectors instead of hanging
    - `auconcreatecontainer` no longer creates containers just to look up proxies
    - `:focusinput -l` no longer remembers insubstantial inputs on page focus ([#5413](https://github.com/tridactyl/tridactyl/issues/5413))
    - `:winopen` no longer opens an extra dummy tab when using container plugins ([#4000](https://github.com/tridactyl/tridactyl/issues/4000))
    - Command line sessions no longer block each other after a command gets stuck ([#3411](https://github.com/tridactyl/tridactyl/issues/3411))
    - `blacklistkeys` now protects keys pressed in iframes (e.g. Google Docs!) ([#5480](https://github.com/tridactyl/tridactyl/issues/5480))
    - `file:` hints now open via `:open` instead of failing silently ([#5080](https://github.com/tridactyl/tridactyl/issues/5080))
    - hints no longer make animated elements disappear ([#3548](https://github.com/tridactyl/tridactyl/issues/3548))
    - `modeindicator` now updates without reloading the page ([#4738](https://github.com/tridactyl/tridactyl/issues/4738))
    - `<C-o>yy` now closes the command line after copying a completion ([#3256](https://github.com/tridactyl/tridactyl/issues/3256))
    - `:back` branches are now sorted by newest members ([#5492](https://github.com/tridactyl/tridactyl/issues/5492))
    - `modeindicator` now works on pages without a `<body>` ([#1163](https://github.com/tridactyl/tridactyl/issues/1163))
    - dark theme hint highlights now remain readable on light pages ([#4420](https://github.com/tridactyl/tridactyl/issues/4420))
    - mode indicator text no longer appears in Firefox's find results ([#2089](https://github.com/tridactyl/tridactyl/issues/2089))
    - `;x` and `;X` now work in same-origin iframes ([#3261](https://github.com/tridactyl/tridactyl/issues/3261))
    - counts are no longer interpreted in ex, ignore, or hint mode ([#1658](https://github.com/tridactyl/tridactyl/issues/1658))
    - content state listeners no longer run when nothing changed ([#2360](https://github.com/tridactyl/tridactyl/issues/2360))
    - completions update when navigating ex command history ([#5109](https://github.com/tridactyl/tridactyl/issues/5109))
    - `:autocmddelete` now works on default autocmds ([#4605](https://github.com/tridactyl/tridactyl/issues/4605))
    - `:tabopen -c` no longer loses its specified container in strict `autocontainmode` ([#3562](https://github.com/tridactyl/tridactyl/issues/3562))
    - `searchurls` now always encode search terms, including `#` ([#5122](https://github.com/tridactyl/tridactyl/issues/5122))
    - `:source` for local files and `:mktridactylrc` now show a helpful error when native is missing ([#3730](https://github.com/tridactyl/tridactyl/issues/3730))
    - `modeindicator` breaks fewer pages ([#821](https://github.com/tridactyl/tridactyl/issues/821))
    - `:tabopen` should no longer open tabs in the wrong window ([#1019](https://github.com/tridactyl/tridactyl/issues/1019))
    - completion results should no longer appear out of order when filtering is slow ([#4584](https://github.com/tridactyl/tridactyl/issues/4584))
    - `:native` now reports detailed native messenger errors ([#1555](https://github.com/tridactyl/tridactyl/issues/1555))
    - commandline now works on more pages with restrictive CSP ([#5474](https://github.com/tridactyl/tridactyl/issues/5474))
    - `:keymap .` works ([#2333](https://github.com/tridactyl/tridactyl/issues/2333))
    - `:extoptions` completions work again ([#5459](https://github.com/tridactyl/tridactyl/issues/5459))
    - `:winopen -c` now opens URLs directly in the specified container ([#5439](https://github.com/tridactyl/tridactyl/issues/5439))
    - configuration saving now preserves values that JSON stringification would discard
    - JS hints now use the same visibility checks as regular hints
    - `:fillcmdline_tmp` no longer blocks completions until its timeout expires ([#5402](https://github.com/tridactyl/tridactyl/issues/5402))
    - `followpagepatterns` now accepts ordered pattern arrays so `]]` and `[[` are less likely to jump straight to the first/last page ([#1730](https://github.com/tridactyl/tridactyl/issues/1730))
    - `:guiset` now reports native messenger write errors ([#2094](https://github.com/tridactyl/tridactyl/issues/2094))
    - beta builds now use the next release's version number rather than claiming to be pre-releases of the previous version
    - `HistoryState` autocmds now trigger reliably on same-document navigation ([#4329](https://github.com/tridactyl/tridactyl/issues/4329))
    - `:q` can no longer report “Message manager disconnected” when closing tabs ([#1528](https://github.com/tridactyl/tridactyl/issues/1528))
    - `:set newtab about:blank` now shows `&centerdot;` rather than `New Tab` as the tab title ([#5021](https://github.com/tridactyl/tridactyl/issues/5021))
    - macOS Firefox profile detection now finds profiles three directories deep ([#1627](https://github.com/tridactyl/tridactyl/issues/1627))
    - Missing `tridactylrc` errors now point to `:help source` and include native error details ([#4088](https://github.com/tridactyl/tridactyl/issues/4088))
    - `xclip` processes now exit after setting the clipboard ([#4822](https://github.com/tridactyl/tridactyl/issues/4822))
    - Native messenger errors now include the underlying error instead of hiding it ([#4827](https://github.com/tridactyl/tridactyl/issues/4827))
    - `:nativeopen` now works with Firefox Nightly on macOS ([#1113](https://github.com/tridactyl/tridactyl/issues/1113))
    - `:tabdiscard` now reports an error when no tab index is provided ([#5400](https://github.com/tridactyl/tridactyl/issues/5400))
    - `modeindicatormodes` now applies to the mode indicator on page load ([#2690](https://github.com/tridactyl/tridactyl/issues/2690))
    - ex-mode (commandline) help works again ([#5044](https://github.com/tridactyl/tridactyl/issues/5044))
    - hidden mode indicators no longer flash while scrolling ([#4760](https://github.com/tridactyl/tridactyl/issues/4760))
    - `find` no longer (fails to) run on Windows during Firefox profile detection ([#2130](https://github.com/tridactyl/tridactyl/issues/2130))
    - `:editor` now works in inputs inside elements with IDs starting with digits ([#2676](https://github.com/tridactyl/tridactyl/issues/2676))
    - `searchurls` now only interpolates documented `%s1-9` placeholders ([#3868](https://github.com/tridactyl/tridactyl/issues/3868))
    - `:js` now works with JavaScript filenames containing `-d` ([#3684](https://github.com/tridactyl/tridactyl/issues/3684))
    - `:guiset hoverlink left` works again after using `none` ([#1957](https://github.com/tridactyl/tridactyl/issues/1957))
    - `halloween` theme no longer breaks commands such as `:hint -y` ([#2025](https://github.com/tridactyl/tridactyl/issues/2025))

- Miscellaneous

    - `:bind` docs now explain how to use Firefox-reserved keys such as `<C-n>` via `about:keyboard` ([#5335](https://github.com/tridactyl/tridactyl/issues/5335))
    - `:help` navigation pane now links to commands, settings, tutorial, editor functions, hint mode commands, and the wiki ([#5194](https://github.com/tridactyl/tridactyl/issues/5194))
    - `:tutor` now has a navigation pane for jumping between tutorial pages ([#5311](https://github.com/tridactyl/tridactyl/issues/5311))
    - `:back` and `:forward` documentation now explains counts and the tab history tree ([#4737](https://github.com/tridactyl/tridactyl/issues/4737))
    - the changelog on the new tab now uses an accessible `<details>` toggle ([#5029](https://github.com/tridactyl/tridactyl/issues/5029))
    - `--tridactyl-search-highlight-color` and `--tridactyl-search-highlight-active-color` can now customise normal and active `:find` highlights ([#3040](https://github.com/tridactyl/tridactyl/issues/3040))
    - `:set` completions can now show column headings for settings, current values, possible values, and documentation ([#966](https://github.com/tridactyl/tridactyl/issues/966))
    - `:goto` completions now show heading trees like `:back` ([#5456](https://github.com/tridactyl/tridactyl/issues/5456))
    - `--tridactyl-container-border` lets themes style mode indicator borders in container tabs ([#3897](https://github.com/tridactyl/tridactyl/issues/3897))
    - `--mode` completions now show descriptions for each binding mode ([#3312](https://github.com/tridactyl/tridactyl/issues/3312))
    - `tri.l` now logs non-promises as well ([#4352](https://github.com/tridactyl/tridactyl/issues/4352))
    - `:tutor` previous-page links are now visible and labelled ([#5026](https://github.com/tridactyl/tridactyl/issues/5026))
    - `:set` now handles arrays in deep settings ([#2918](https://github.com/tridactyl/tridactyl/issues/2918))
    - `:hint -W` now uses the content script where possible ([#3569](https://github.com/tridactyl/tridactyl/issues/3569))
    - `:source` now runs config updaters after importing an RC file ([#1905](https://github.com/tridactyl/tridactyl/issues/1905))
    - builds now work in Git worktrees ([#5442](https://github.com/tridactyl/tridactyl/issues/5442))
    - `scripts/sign nosignbeta` no longer includes an `update_url` in unsigned beta builds ([#2720](https://github.com/tridactyl/tridactyl/issues/2720))
    - `:drawingstart` now shows a cursor to help you aim better ([#4006](https://github.com/tridactyl/tridactyl/issues/4006))
    - Tridactyl no longer errors when `PerformanceObserver` is unavailable ([#1757](https://github.com/tridactyl/tridactyl/issues/1757))
    - `wine-pyinstaller.sh` now supports newer versions of Wine and 7-Zip ([#1240](https://github.com/tridactyl/tridactyl/issues/1240))
    - `:tutor` no longer breaks the troubleshooting guide link ([#5209](https://github.com/tridactyl/tridactyl/issues/5209))
    - `scripts/version.js bump` now adds release notes to release commits and dates the matching changelog entry ([#517](https://github.com/tridactyl/tridactyl/issues/517))
    - `:source` and `:mkt` now document RC file syntax, search precedence, and one-time loading ([#3902](https://github.com/tridactyl/tridactyl/issues/3902))
    - `:tutor` now documents container commands, settings, and automatic container assignment ([#3008](https://github.com/tridactyl/tridactyl/issues/3008))
    - `:mode ignore` docs now explain its toggle shortcuts and how to bind custom toggles ([#2108](https://github.com/tridactyl/tridactyl/issues/2108))
    - website builds work again ([#5144](https://github.com/tridactyl/tridactyl/issues/5144))
    - `:colourscheme` docs now link to the custom theme guide and explain flashes while themes load ([#3466](https://github.com/tridactyl/tridactyl/issues/3466))
    - `profiledir` docs now explain automatic detection, absolute paths, and Firefox `-P`/`--profile` launch options ([#4149](https://github.com/tridactyl/tridactyl/issues/4149))
    - `:ttsread` docs now explain how to read selected text in visual mode ([#3848](https://github.com/tridactyl/tridactyl/issues/3848))
    - `yarn pretty` now shows the changes it makes to staged files ([#2514](https://github.com/tridactyl/tridactyl/issues/2514))
    - Firefox's "Never remember history" setting now has a troubleshooting note explaining its effect on Tridactyl's persistent state ([#4302](https://github.com/tridactyl/tridactyl/issues/4302))
    - daily E2E runs now test the latest Firefox beta to help us catch broken stuff before it reaches most users ([#5241](https://github.com/tridactyl/tridactyl/issues/5241))
    - `:open site [query]` jsurl added to example .tridactylrc for searching the current website ([#5358](https://github.com/tridactyl/tridactyl/issues/5358))
    - `:tutor` now explains how to inspect current bindings with `:viewconfig` and `:bind --mode=` ([#3228](https://github.com/tridactyl/tridactyl/issues/3228))
    - e2e tests now exit reliably after cleaning up all WebDriver instances
    - `browser-targets.json` now shares minimum browser versions between build targets
    - `:hint -k` and `:hint -K` documentation now clearly distinguishes deleting from hiding ([#5052](https://github.com/tridactyl/tridactyl/issues/5052))
    - TypeScript 6 and TypeDoc 0.28 are now used for builds and documentation (I had been putting this off for ~5 years) ([#5450](https://github.com/tridactyl/tridactyl/issues/5450))
    - `proxy` now documents that `none` disables the proxy and works with `:autocontain` ([#4226](https://github.com/tridactyl/tridactyl/issues/4226))
    - `:updatenag` now gives the correct steps for checking Tridactyl updates in Firefox ([#5415](https://github.com/tridactyl/tridactyl/issues/5415))
    - `:rssexec` e2e test is less flaky ([#5426](https://github.com/tridactyl/tridactyl/issues/5426))
    - on Windows `:nativeinstall` now suggests running cmd.exe as Administrator if installation fails ([#4512](https://github.com/tridactyl/tridactyl/issues/4512))
    - AMO credentials are now hidden from `scripts/sign` logs ([#3633](https://github.com/tridactyl/tridactyl/issues/3633))
    - `:guiset` help now explains how to find and set `profiledir` on Windows ([#4462](https://github.com/tridactyl/tridactyl/issues/4462))
    - `:bind` and `:gobble` docs now explain dynamic keybindings and the `:command` alias workaround ([#1985](https://github.com/tridactyl/tridactyl/issues/1985))
    - `profiledir` docs now warn against surrounding paths with quotes ([#4726](https://github.com/tridactyl/tridactyl/issues/4726))
    - E2E tests no longer fail when Google blocks CI
    - argument parsing now applies default option values consistently to `:find`, `:findnext`, `:tabprev`, and `:keyfeed` ([#4711](https://github.com/tridactyl/tridactyl/issues/4711))
    - `editorcmd` now documents `start /wait` for Windows editors ([#5072](https://github.com/tridactyl/tridactyl/issues/5072))
    - `:source` and `:sanitise` docs now warn that persisted settings may briefly apply at startup ([#1179](https://github.com/tridactyl/tridactyl/issues/1179))
    - RC file documentation now links to GitHub code search for other `tridactylrc` files ([#4736](https://github.com/tridactyl/tridactyl/issues/4736))
    - `<C-[>` is now unbound in the exemplar `.tridactylrc`, freeing it for back navigation ([#5365](https://github.com/tridactyl/tridactyl/issues/5365))
    - Normal mode tutorial now clearly explains keyword searches ([#5051](https://github.com/tridactyl/tridactyl/issues/5051))
    - `:discardall` now aliases the built-in `:tabdiscard --all` command in the example config ([#5479](https://github.com/tridactyl/tridactyl/issues/5479))
    - `:js` help now correctly displays examples for custom ex-commands
    - `searchurls.julia` has been removed from the example RC

Thanks to all of our contributors for this release: Oliver Blanthorn, Chic-Tweetz, mmustafasenoglu, gholk, glacambre, paris and wskozel.

Extra special thanks go to mmustafasenoglu and wskozel who both contributed for the first time.

Last, but not least - thank you to everyone who reported the issues we closed in this release: bovine3dom, glacambre, people who have since deleted their GitHub accounts, notJerl, cmcaine, GHolk, Rummskartoffel, indeedwatson, Monirzadeh, mozbugbox, alerque, antonva, avalonv, Chic-Tweetz, eboth, ELLIOTTCABLE, guy-who-googles, hejops, jkavalec, laur89, MoltenMonster, ok-nick, pataquets, rodrigo-morales-1, saulrh, utsekaj42, wavexx, a7u8, aavogt, abo-abo, adamency, alanoliveira, albert-ying, Alice-Null, allanwind, anmolmathias, antran22, Artikool5, asteroidcow, azatoth, bambams, basbebe, Betelgeuse1, bew, b-jazz, bodograumann, booxter, borazan, boriselec, bpmcl, braham-snyder, caputchinefrobles, cheonhyangzhang, codesoverhill, CoelacanthusHex, Corallus-Caninus, cprn, davidsierradz, dioobspm, dmpetroff, dotancohen, dwawlyn, dwolf42, e00E, farnoy, Flimm, frenchbeard, gcmurray1990, Geobert, glm4610, glyh, groundstation1, gsbabil, henk84, henkworks, herrsimon, heyzec, HiPhish, hnorkowski, hpfr, IceAsteroid, In-line, j-a-m-l, jcrowgey, jimrs, jjeffrey, j-lakeman, Jonta, jumper047, KAGEYAM4, kartynnik, kpeejp, kwstannard, lyterk, madduck, make-github-pseudonymous-again, Massimo-B, Mercvre80, mesaprotector, mightyiam, MilanV, minhduc0711, mist, mocamp3, mornfall, Nampuk, natefabian18, newbould5, nicksavoie, nkitsaini, noctuid, noib3, nullpointseven, numirias, nvkrishna77, otterlover24, OwenAR, oxysoft, pengowen123, pickfire, pmaziere, pnelson, pufferfish-connoisseur, pzzxqj, quisquama, Qwyntex, radgeRayden, raffaelemancuso, ricardoseriani, robertgzr, russsharek, rutgersc, scottjad, silmeth, solna86, stagnation, StarTroop, stef, synaps13, Terx37, ThinkChaos, timokau, timss, tom-mi, user471, Utopiah, vaibhav423, Vftdan, vulns-are-features-too, waterbear5, whnr, winchendonsprings, WloHu, Wolfbeta, WolodiaM, x-ji, xNyfPtx, ybc37, yegle and zsugabubus!
2026-08-28 11:08:42 +02:00
Oliver Blanthorn 39e23d58cf
Update changelog for 1.25.0 2026-08-28 11:08:39 +02:00
Robert Hill e04d35fe87 register @flag/@endflags in metadata typedoc pass
build.sh runs a second, separate typedoc invocation to generate
.metadata.generated.json that never got the --blockTags fix from
0b6492bd, so it hit the same unknown-tag issue.
2026-08-28 00:09:22 -04:00
Robert Hill 0b6492bd85 Register @flag/@endflags as recognized TypeDoc block tags
Silences the "unknown block tag" warning TypeDoc logged for every
@flag/@endflags use, since it didn't recognize them.

--blockTags replaces TypeDoc's whole default list rather than appending,
so we read the defaults at runtime and re-pass them alongside our two
custom tags.
2026-08-27 23:44:12 -04:00
Robert Hill fea4865033 Rewrite hint's doc comment using @flag/@endflags
- Replaces the {{tridactyl-flag-list}} marker and prose flag bullets
with @flag tags and two @endflags boundaries. This keeps the flags
rendered where they're placed in the code without the rest of the docs
getting swallowed into the last flag's content.
- Moves @param below the content summary to match where the content is
rendered on the Help doc.
- Also fixes -pipe and -W, which had their flag name wrapped in
backticks and were silently dropped by the parser.
2026-08-27 23:18:44 -04:00
Robert Hill 14ade554b5 Split @flag descriptions and add @endflags for multi-section docs
- @flag text now splits on the first blank line into a short label
(shown in the flag list) and longer elaboration prose rendered beneath
it.
- @endflags closes a run of @flag tags and flushes them. This prevents
TSDoc from swallowing any trailing text into the last @flag.
- Markdown headings inside trailing content are also kept out of the
page's sidebar nav to avoid cluttering it with each excmd's subsections.
2026-08-27 23:08:33 -04:00
Robert Hill e744f5651d Rename @arg tag to @flag
@arg is a bit ambiguous since an "argument" can hold anything.

@flag better matches what this tag actually documents (excmd
command-line flags).
2026-08-27 14:44:35 -04:00
Robert Hill a7d6115688 Add @arg tag support for documenting excmd flags
Extracts @arg -flag description tags from excmd JSDoc into a flags map
in the generated metadata, and renders them as one bullet list in
`:help`.

A docstring can drop a `{{tridactyl-arg-list}}` marker anywhere in its
prose to control where that list renders, since the @arg tags
themselves must stay at the end of the comment (TSDoc runs a block
tag's content until the next tag).

Annotates `hint` as the first user of this new format.

Related to #5397 (which-key), split out per review discussion since
which-key depends on this.
2026-08-26 23:49:37 -04:00
Oliver Blanthorn 62d4cb89e3
Update changelog for 1.25.0 2026-08-24 12:57:01 +02:00
Oliver Blanthorn b9e85e6dec
Fix #5510: make e2e headless by default 2026-08-22 11:32:03 +02:00
Oliver Blanthorn 23fcba50d3
Fix #5503: add resize observer 2026-08-22 11:29:40 +02:00
Oliver Blanthorn 51b880173d
Fix #5021 again: replace blank/tofu with dot
The egyptian thing was just a hack until Mozilla noticed it
and on many systems showed up as tofu. So we'll admit defeat
and replace it with a visible dot
2026-08-21 12:44:37 +02:00
Oliver Blanthorn e771471e8e
Update thanks to include people who filed issues 2026-08-03 09:18:49 +02:00
Oliver Blanthorn 87bb006f14
Add non-ai google and newspaper theme to rc 2026-08-02 10:21:42 +02:00
Oliver Blanthorn 50ff8dc6c4
Update changelog for 1.25.0 2026-08-01 11:55:55 +02:00
Oliver Blanthorn 63bb26908f
Fix newtab on older FF versions 2026-08-01 11:52:51 +02:00
Oliver Blanthorn 1f4c5a2c33
Fix #5492: sort :back by newest members 2026-07-31 21:46:31 +02:00
Oliver Blanthorn 20f60a2119
Make :colourscheme --url reuse downloaded themes
Simplifies using `:colourscheme` in RC files without having
to redownload the theme every time. You can use `--update`
to force an update
2026-07-31 21:29:46 +02:00
Oliver Blanthorn 6f8cee4e40
Fix #5490: persist custom themes after config wipe 2026-07-31 20:25:05 +02:00
Oliver Blanthorn 9c8c15ff77
Send tab updates to active tabs
In theory browser.runtime should have worked fine
but it doesn't. So go back to browser.tabs instead

Fix #5487

I hope
2026-07-28 20:13:00 +02:00
Oliver Blanthorn a9f2a523c0
Update changelog for 1.25.0 2026-07-28 13:32:14 +02:00
Oliver Blanthorn d9e727780b
Add runwithprofile to run in copy of real profile 2026-07-28 13:12:15 +02:00
Oliver Blanthorn dfe15d12a7
Avoid serialising unused args in messages 2026-07-28 11:26:07 +02:00
Oliver Blanthorn 20372d6a7d
Cache tab/taball options for faster filtering 2026-07-28 10:39:58 +02:00
Oliver Blanthorn 912b718755
Update completions in unfocused windows
Fix trailing debounce too
2026-07-28 09:37:04 +02:00
Oliver Blanthorn 4e55951e55
Revert :tab initialposition to 'top'
I think it's too counter-intuitive for everyday use
at the moment. The visual feedback isn't quite there
2026-07-27 22:54:14 +02:00
Oliver Blanthorn 9422bd8e5b
Fix selection fighting on startup with many tabs
Also, modestly unify Tab + TabAll completions
2026-07-27 22:48:18 +02:00
Oliver Blanthorn af727c7b8d
Fix GitHub gi 2026-07-27 21:26:35 +02:00
Oliver Blanthorn 903c148011
Actually start using the RC file again 2026-07-27 21:14:45 +02:00
Oliver Blanthorn cdb99fce76
Update changelog for 1.25.0 2026-07-27 19:57:34 +02:00
Oliver Blanthorn 46568e8af0
Merge branch 'feat/random-hintnames' 2026-07-27 19:56:42 +02:00
Oliver Blanthorn cf78876e33
Explain hardcoded hintchars 2026-07-27 19:55:35 +02:00
Oliver Blanthorn c8103dc0f6
Concatenate word hintnames if we need more 2026-07-27 19:54:33 +02:00
Oliver Blanthorn d50ddc38f5
Bikeshed hintname setting name 2026-07-27 19:53:48 +02:00
Oliver Blanthorn cb065cf04f
Update changelog for 1.25.0 2026-07-27 19:26:10 +02:00
Oliver Blanthorn 4af189ccd3
Add :find binds to RC and bikeshed settings 2026-07-27 19:22:59 +02:00
Oliver Blanthorn b5ba02e695
Add :find completions back
Also improve perf:

- index headings for breadcrumbs

- don't clone ranges to make highlights

- fiddle with debounce
2026-07-27 19:21:24 +02:00
Oliver Blanthorn e8d85058f7
Bikeshed :find help 2026-07-27 19:21:24 +02:00
Oliver Blanthorn 5d0c10ea37
Fix :find -r in iframes 2026-07-27 19:21:24 +02:00
Oliver Blanthorn 475ae04e79
Restore incsearch 2026-07-27 19:21:24 +02:00
Oliver Blanthorn 0ab3f5bf62
Add more words to the glossary 2026-07-27 13:35:03 +02:00
Oliver Blanthorn edf442fe67
Update outdated :help docstring 2026-07-27 13:35:03 +02:00
Oliver Blanthorn 82985ae14c
Fix #2980: add MVP :define and glossary
Now I just have to write all the definitions :(
2026-07-27 13:35:03 +02:00
Oliver Blanthorn 1ad5c34ac6
Fix #1528: silence errors for messages sent to closed tabs 2026-07-27 10:16:21 +02:00
Oliver Blanthorn 224a8930e1
Fix #1774: add :unbind --all
Also, improve `:unbind --recursive` to apply to inherited
keymaps too
2026-07-27 10:13:57 +02:00
mmustafasenoglu 169613e242 feat: add 'random' option to hintnames config 2026-07-27 09:06:39 +03:00
mmustafasenoglu b8c41e0c24 feat: add random hintnames with built-in English word list 2026-07-27 09:06:24 +03:00
Oliver Blanthorn dddf6d9fc6
Fix #4329: use webNavigation API
This replaces the unreliable monkey patched history API
2026-07-26 23:36:59 +02:00
Oliver Blanthorn 6b52c0fa36
Fix #4993: add countaware for disabling counts per URL/mode 2026-07-26 22:28:03 +02:00
Oliver Blanthorn 03320b51ae
Fix #2370: add <C-o> temporary ignore bind in insert mode
Enabled by #5475
2026-07-26 22:27:40 +02:00
Oliver Blanthorn 7e14dd2e0d
Fix #3649: add :autocontaindelete 2026-07-26 22:18:31 +02:00
Oliver Blanthorn 2c97a45f28
Fix #4746: add :set hinthideobscured
I'm leaving this defaulted to false for now because
many so-called 'consent' popups count as obscuring
the content you want to click behind them, and Tridactyl
is on the side of the user who wants to click things
2026-07-26 21:36:35 +02:00
Oliver Blanthorn 2aec1280e7
Fix #1612: add :find -r for regex search 2026-07-26 21:23:50 +02:00
Oliver Blanthorn 38183e472a
Fix #3562: make tabopen -c bypass autocontain 2026-07-26 21:14:25 +02:00
Oliver Blanthorn 5af3d20005
Fix #3124: remember last input per session 2026-07-26 19:26:37 +02:00
Oliver Blanthorn 09131a269f
Fix #5021: don't let ff give newtab a title
U+13442 EGYPTIAN HIEROGLYPH HALF BLANK renders as nothing
and doesn't get replaced with 'New Tab' when newtab is set
to about:blank. it is a stupid hack, yes.
2026-07-26 18:54:06 +02:00
Oliver Blanthorn f094c3f4dc
Fix #5061: document dead-key conflict in troubleshooting 2026-07-26 18:50:43 +02:00
Oliver Blanthorn c55948419f
Fix #1627: increase profile maxdepth on OSX 2026-07-26 18:33:02 +02:00
Oliver Blanthorn ebd4f13bf3
Fix #2935: prefer exact match with vimp hints 2026-07-26 13:59:09 +02:00
Oliver Blanthorn e4e1357cb4
Fix #1019: don't let firefox guess windows 2026-07-26 13:33:45 +02:00
Oliver Blanthorn 143d8c70df
Fix pre regression introduced in e7993f21 2026-07-26 12:10:03 +02:00
Oliver Blanthorn d9851b2f72
Fix #5044: add ex-mode fns back to help 2026-07-26 12:10:03 +02:00
Oliver Blanthorn 0b4f3ac665
Fix #1555: catch missing native errors 2026-07-26 12:01:41 +02:00
Oliver Blanthorn 77056b37c4
Fix #4088: improve rc errors 2026-07-26 11:55:53 +02:00
Oliver Blanthorn 7af3a596eb
Fix #4584: make completions queue politely 2026-07-26 11:34:12 +02:00
Oliver Blanthorn f16e1176a4
Fix #4726: warn against quotes in profiledir 2026-07-26 11:28:17 +02:00
Oliver Blanthorn b0ba29586d
Fix #1179: improve :source / :sanitise docs 2026-07-26 10:32:34 +02:00
Oliver Blanthorn 9f93d3f7d1
Fix broken mock tests 2026-07-26 10:31:02 +02:00
Oliver Blanthorn 3322fe2271
Fix mangled :issue docstring 2026-07-26 10:27:29 +02:00
Oliver Blanthorn ff049787b8
Fix #1352: show excmds for binds on :help 2026-07-26 10:27:03 +02:00
Oliver Blanthorn e66125794d
Fix #4760: display none-ify modeindicator 2026-07-25 22:37:40 +02:00
Oliver Blanthorn 3a51611161
Update tests and docs 2026-07-25 22:35:45 +02:00
Oliver Blanthorn 0a9e073aaf
Fix #5122: always encode searchurls
I guess this could break more exotic user searchurls?
2026-07-25 22:23:39 +02:00
Oliver Blanthorn e7993f216c
Fix #1933: make :find work on :viewsource and :reader
This is quite a big piece of work that moves :viewsource out
into its own tab and iframe, because Firefox can't find text
the old way. Then, we fix an iframe bug where :find was not
working across iframes, such as on :reader
2026-07-25 21:53:00 +02:00
Oliver Blanthorn 8dd6e14550
Fix #4822: let xclip exit 2026-07-25 21:38:59 +02:00
Oliver Blanthorn b75db50a5f
Make E2E succeed even if Google blocks our CI 2026-07-25 21:31:50 +02:00
Oliver Blanthorn a24e83be86
Fix browser.search regression
Somehow we made a slightly flakey search failure
deterministic with one of our changes
2026-07-25 21:31:26 +02:00
Oliver Blanthorn a544a0dba1
Fix #3416: match unambiguous prefixes of excommands 2026-07-25 19:54:31 +02:00
Chic-Tweetz 1b16054cd1
Fix #5474: resolve iframeReady on load 2026-07-25 18:07:49 +02:00
Oliver Blanthorn 87c15254d0
Fix #4668: support tabsort mru in :taball 2026-07-25 17:49:50 +02:00
Oliver Blanthorn 35d6eb571c
Fix #2276: add historysort frecency|frequency|recency 2026-07-25 14:09:55 +02:00
Oliver Blanthorn 3387a4f864
Fix #822: add browser button to toggle superignore
Ideally this would inherit the user's theme but I don't think
I care enough to do that
2026-07-25 12:41:21 +02:00
Oliver Blanthorn d0739c51d9
Fix #944: ex completions fall back to doc search 2026-07-25 12:36:44 +02:00
Oliver Blanthorn d27a2acaf9
Fix #5232: add :guiset sidebar hide 2026-07-25 12:32:14 +02:00
Oliver Blanthorn d38ea7fba5
Fix #3506: make g; skip closed tabs 2026-07-25 12:30:34 +02:00
Oliver Blanthorn 51775ac49f
Fix #5049: ga picks last audible tab
Also, add tri.webext.getLastAudibleTab() for use in scripts
2026-07-25 08:34:41 +02:00
Oliver Blanthorn 8ed0f11d59
Fix #3407: add text.unix_word_rubout 2026-07-25 08:05:02 +02:00
Oliver Blanthorn 91f1ff22b1
Fix #3730: throw errors on source/mkt without native 2026-07-25 08:03:17 +02:00
Oliver Blanthorn a51d72a2a6
Fix #3839: add :js -rc with in-memory cache 2026-07-25 07:58:51 +02:00
Oliver Blanthorn 77d3635c9b
Fix #3633: hide AMOKEYS from logs 2026-07-24 22:57:53 +02:00
Oliver Blanthorn 6d858e3389
Fix #2025: only hide iframe border in halloween theme 2026-07-24 22:55:40 +02:00
Oliver Blanthorn a1e247528b
Fix #4736: mention GitHub code search for RC files 2026-07-24 22:39:04 +02:00
Oliver Blanthorn 19b0ef104b
Fix #1985: improve gobble docs 2026-07-24 22:38:17 +02:00
Oliver Blanthorn 9a2277f1fe
Remove julia search from RC
replaced by `site` and it was kind of annoying
2026-07-24 22:36:41 +02:00
Oliver Blanthorn ebaa6038dc
Fix #5479: replace discardall with real command 2026-07-24 22:36:41 +02:00
Oliver Blanthorn a03f58442e
Add <C-[> unbind to exemplar rc
See #5365
2026-07-24 22:36:41 +02:00
Oliver Blanthorn f08e640423
Fix #3411: make commandline sessions independent 2026-07-24 20:39:59 +02:00
Oliver Blanthorn b85b2279c1
Fix #5051: make tutor search docs more explicit 2026-07-24 20:33:57 +02:00
Oliver Blanthorn 3453ba767c
Fix #1669: modeindicatorshowkeys for hint filter 2026-07-24 20:31:44 +02:00
Oliver Blanthorn e8a263aa08
Fix #821: defer mode indicator until document load
This is a bit annoying on slow loading pages but DOMContentLoaded
was still breaking some websites :(
2026-07-24 19:01:51 +02:00
Oliver Blanthorn 4996ffab3e
Fix #4724: add :quickmarkremove 2026-07-24 18:57:19 +02:00
Oliver Blanthorn 43183deb2a
Fix #1730: make followpagepatterns ordered arrays 2026-07-24 18:54:08 +02:00
Oliver Blanthorn 89bdcc923e
Fix #5480: support blacklistkeys in iframes 2026-07-24 18:53:43 +02:00
Oliver Blanthorn 1ed0fdea4f
Fix #3751: style selected hint tag 2026-07-24 18:47:01 +02:00
Oliver Blanthorn 4193c11862
Fix #137: add :dialog for opening about: pages 2026-07-24 18:41:13 +02:00
Oliver Blanthorn e7bcc3cae1
Display mysterious native errors to the user
Related: #4827
2026-07-24 17:49:22 +02:00
Oliver Blanthorn 6a207ab39f
Fix #4711: add defaults to arg parser 2026-07-24 17:47:53 +02:00
Oliver Blanthorn 48d4f33d2a
Fix #3473: stop :updatenative from lying 2026-07-24 17:46:10 +02:00
Oliver Blanthorn de37f815e9
Fix #5472: add :sanitise tridactylconfig 2026-07-24 17:44:22 +02:00
Oliver Blanthorn 6eb3073144
Add ,i issue bind to tridactylrc 2026-07-24 17:44:22 +02:00
Oliver Blanthorn 9dbd179929
Fix #5080: intercept file: hints and fall back to :open 2026-07-24 17:27:45 +02:00
Oliver Blanthorn 469fe06b69
Fix #1113: support :nativeopen on FF Nightly on OSX 2026-07-24 17:22:35 +02:00
Oliver Blanthorn aabb26c937
Fix #4462: improve :guiset help for Windows 2026-07-24 17:19:55 +02:00
Oliver Blanthorn 8bb2b2110b
Fix #5400: throw error if trying to discard current tab 2026-07-24 17:05:58 +02:00
Oliver Blanthorn ece3c4fd75
Fix #2690: respect modeindicatormodes on load 2026-07-24 17:03:20 +02:00
Oliver Blanthorn c5f49d0d5d
Fix #615: add :downloads alias for about:downloads 2026-07-24 16:57:46 +02:00
Oliver Blanthorn a98d724d25
Fix #5072: document start /wait on Windows 2026-07-24 16:55:25 +02:00
Oliver Blanthorn 9890b0a1bd
Give beta builds versions from the future
Related: #714
2026-07-24 16:22:57 +02:00
Oliver Blanthorn 8bd4144ff7
Update changelog for 1.25.0 2026-07-24 14:29:55 +02:00
Oliver Blanthorn db892be9bc
Support vanity release names 2026-07-23 21:09:10 +02:00
Oliver Blanthorn 1ac287cfc8
Fix #4681: add time estimate to :reader 2026-07-23 20:56:09 +02:00
Oliver Blanthorn a329e17533
Fix #1957: fix none -> left display 2026-07-23 20:50:46 +02:00
Oliver Blanthorn c889f9a78a
Fix #2334: allow literal backslash in rc 2026-07-23 20:47:17 +02:00
Oliver Blanthorn 8628390720
Fix #966: give table header cells 2026-07-23 20:39:23 +02:00
Oliver Blanthorn 0184b24c91
Fix #5029: use summary tag for changelog 2026-07-23 20:37:43 +02:00
Oliver Blanthorn 4c67dc3d54
Fix #4512: recommend running cmd as admin 2026-07-23 20:35:38 +02:00
Oliver Blanthorn ec51dee8ae
Fix #2094: catch code from Native.write 2026-07-23 20:33:06 +02:00
Oliver Blanthorn 2d63db45fc
Fix #4931: don't style pre in reader 2026-07-23 20:31:20 +02:00
Oliver Blanthorn fa820b1b18
Fix #4006: give :drawingstart a crosshair 2026-07-23 19:47:27 +02:00
Oliver Blanthorn 548c41c9b4
Fix #2293: make container choice deterministic 2026-07-23 19:46:15 +02:00
Oliver Blanthorn 7d1ed019e5
Fix #4159: add :set repeatblacklist []
ex-commands in this array are ignored by :repeat
2026-07-23 19:45:19 +02:00
Oliver Blanthorn e0dfff51ca
Fix #3973: batch tab changes
Also, only send them to the active tab
2026-07-23 19:43:01 +02:00
Oliver Blanthorn f5ce92069f
Fall back to :apropos matches for :help 2026-07-23 18:03:10 +02:00
Oliver Blanthorn 902b3b6e58
Fix #1175: use .includes if :set completions empty 2026-07-23 18:03:09 +02:00
Oliver Blanthorn 385b818c44
Fix #5026: stop hiding :tutor previous page links 2026-07-23 17:57:26 +02:00
Oliver Blanthorn 89dfecec09
Fix 1158: start :tab from current by default
Override this with `:set completions.Tab.initialposition top`
2026-07-23 17:55:26 +02:00
Oliver Blanthorn 2bf0e5bd54
Fix #5311: add nav pane to :tutor 2026-07-23 17:32:27 +02:00
Oliver Blanthorn f5f9f2708a
Fix #217: expose more ttscontrol controls 2026-07-23 17:29:16 +02:00
Oliver Blanthorn 1f4ada905f
Fix #3902: improve rc docs 2026-07-23 17:29:01 +02:00
Oliver Blanthorn 87eb964638
Fix #3466: improve colourscheme docs 2026-07-23 17:21:00 +02:00
Oliver Blanthorn f6cea640c1
Fix #4164: add pgup/pgdown completion binds
I don't _love_ this. sometimes you can jump down a page and your
selection goes to the top of the completions, so you miss basically
an entire page of completions
2026-07-23 17:02:08 +02:00
Oliver Blanthorn da03de2109
Fix #4000: use a safer URL for dummy tabs 2026-07-23 16:54:07 +02:00
Oliver Blanthorn 07fea47e43
Fix #4437: disable insert detection in nmode 2026-07-23 16:44:33 +02:00
Oliver Blanthorn 97145f3b68
Fix #2918: allow arrays as deep settings 2026-07-23 16:42:36 +02:00
Oliver Blanthorn ff5511691a
Fix #414: update modeindicatoron focus change 2026-07-23 16:37:55 +02:00
Oliver Blanthorn 25b18c6e8d
Fix #4649: autoselect tab groups 2026-07-23 16:35:48 +02:00
Oliver Blanthorn 2e70faefd7
Fix #3806: add :findrc 2026-07-23 15:56:14 +02:00
Oliver Blanthorn caa555e540
Fix #5209: make sed only target relative links 2026-07-23 15:13:37 +02:00
Oliver Blanthorn 432d38a12d
Fix #3548: hints pause rather than remove animations 2026-07-23 15:12:42 +02:00
Oliver Blanthorn f0037b1551
Fix #5475: make nmode ignore initial keyup 2026-07-23 15:10:32 +02:00
Oliver Blanthorn ceccbce949
Hide more things from repeat 2026-07-23 14:50:52 +02:00
Oliver Blanthorn 837dbf5f2e
Fix #1234: add :set modeindicatorshowlastex 2026-07-23 14:50:51 +02:00
Oliver Blanthorn 18f9a2c9b8
Fix #4777: add :tabclose --match
Closes all matching tabs
2026-07-23 14:50:12 +02:00
Oliver Blanthorn 91feb5eb7a
Fix #3008: improve container tutorial 2026-07-23 14:03:49 +02:00
Oliver Blanthorn 28c91c6a53
Fix #5194: add links to :help nav pane 2026-07-23 14:02:21 +02:00
Oliver Blanthorn 8241b63aab
Fix #3040: add tridactyl-search-highlight-{active-,}color 2026-07-23 14:02:21 +02:00
Oliver Blanthorn 03528e983c
Fix #4149: improve profiledir docs 2026-07-23 14:02:21 +02:00
Oliver Blanthorn f40a9a98c0
Fix #3848: improve ttsread docs 2026-07-23 14:02:21 +02:00
Oliver Blanthorn d396ec1ba0
Fix #5052: improve hint -k/K docs 2026-07-23 14:02:21 +02:00
Oliver Blanthorn b67d84cd0e
Fix #2702: restore excmd mock test 2026-07-23 14:02:21 +02:00
Oliver Blanthorn 2c0d8322d3
Fix #4738: watch modeindicator without needing reload 2026-07-23 14:02:21 +02:00
Oliver Blanthorn 7af0c160c6
Fix #3312: add descriptions to mode completions 2026-07-23 14:02:21 +02:00
Oliver Blanthorn c7281edab6
Fix #1103: catch failed loadPrefs 2026-07-23 14:02:21 +02:00
Oliver Blanthorn ae2abcd412
Fix #1415: route restricted URLs through :nativeopen 2026-07-23 13:28:39 +02:00
Oliver Blanthorn 7942896402
Fix #3174: resize/reselect on tab/taball 2026-07-23 13:27:46 +02:00
Oliver Blanthorn 40be93ad12
Fix #4697: improve :editor errors 2026-07-23 13:22:19 +02:00
Oliver Blanthorn 9216afb75d
Fix #3256: make <C-o>yy less weird 2026-07-23 13:20:36 +02:00
Oliver Blanthorn ae7ba91d1a
Fix #5320: add helpful error to expired reader 2026-07-23 13:14:34 +02:00
Oliver Blanthorn 2d750e300f
Fix #2514: make pretty more communicative 2026-07-23 13:13:25 +02:00
Oliver Blanthorn e032d16da6
Fix #2418: hintshift also selects next element 2026-07-23 13:09:52 +02:00
Oliver Blanthorn 5887493a8d
Fix #2082: respect leavegithubalone everywhere except ignore 2026-07-23 12:59:21 +02:00
Oliver Blanthorn b40b78c8f6
Fix #3569: route hint -W via content 2026-07-23 12:57:38 +02:00
Oliver Blanthorn c3c5ec5b62
Fix #5066: add container completions 2026-07-23 12:56:55 +02:00
Oliver Blanthorn 4f3889d47d
Fix #1905: run config update after RC import 2026-07-23 12:51:41 +02:00
Oliver Blanthorn edb5a60799
Fix #834: remove (pointless) jump position restoration
Firefox handles this already, no point in doing it but worse
2026-07-23 12:39:37 +02:00
Oliver Blanthorn 926f906ae4
Fix #3025: add :containercreate --or-update 2026-07-23 12:38:30 +02:00
Oliver Blanthorn d471bd568d
Fix #4737: document :back history tree 2026-07-23 11:05:25 +02:00
Oliver Blanthorn dd1820b471
Fix #4166: bmark -b completions 2026-07-23 11:05:25 +02:00
Oliver Blanthorn 86d205f84f
Fix #2130: don't use find on windows 2026-07-23 11:05:25 +02:00
Oliver Blanthorn 7d5c9cf083
Fix #4302: document permanent private mode problems 2026-07-23 11:05:25 +02:00
Oliver Blanthorn 7811e5f531
Fix #1240: support newer version of wine/7zip
This is kind of dumb because we're assuming all new versions will
forever be backwards compatible. But it is less dumb than assuming
all new minor versions will break everything
2026-07-23 10:22:24 +02:00
Oliver Blanthorn b22f3a708e
Fix #5241: test FF beta daily 2026-07-23 10:20:44 +02:00
Oliver Blanthorn 7888e42385
Fix #772: do not assume windowIds are static
Man this bug has been annoying me for like eight years.

Root cause: windowIds can change. If the window ID changed we
fell back to reopening a window. which is obviously mad in
retrospect.
2026-07-23 10:19:19 +02:00
Oliver Blanthorn 40dfc79dd2
Fix #1109: add Doc{Blur,Focus} autocmd events 2026-07-23 10:12:39 +02:00
Oliver Blanthorn 63deefc23c
Fix #517: make release commits more useful 2026-07-23 10:10:07 +02:00
Oliver Blanthorn 008c02b1e1
Fix #3581: zz respects default zoom 2026-07-23 08:22:06 +02:00
Oliver Blanthorn 7b054aebb0
Fix #4420: improve dark hint bg contrast 2026-07-23 08:20:59 +02:00
Oliver Blanthorn 0162446b9f
Put status indicator in documentElement rather than body
Half of issue #1163
2026-07-23 08:14:31 +02:00
Oliver Blanthorn 3a4fa48a46
Fix #5364: add js hints to elements with onclick 2026-07-23 08:09:15 +02:00
Oliver Blanthorn 84ac7c87ec
Fix #1236: clean up hints on mode change
This will break people using `nmode` to enter ignore mode briefly,
but I don't know if anyone does that
2026-07-23 08:08:33 +02:00
Oliver Blanthorn e098d4e281
Fix #2712: keyseq support for blacklistkeys
As a bonus, protect all `:bind --mode=browser` binds
2026-07-23 00:11:33 +02:00
Oliver Blanthorn 7ac426ed95
Fix #5246: add tabnext --skip-discarded 2026-07-23 00:02:17 +02:00
Oliver Blanthorn 5b78e75a0c
Fix #2720: remove update_url from unsigned extensions 2026-07-22 23:20:19 +02:00
Oliver Blanthorn c219e92bf9
Fix #4741: add :set searchurlopen 2026-07-22 23:19:23 +02:00
Oliver Blanthorn 6186d32e38
Fix #5358: add search this site example 2026-07-22 23:18:07 +02:00
Oliver Blanthorn af5efbcb3f
Fix #3890: use selectionRange rather start,end
I can't actually reproduce #3890 but this kind of thing
has caused issues before so let's pretend it was the
problem
2026-07-22 23:15:27 +02:00
Oliver Blanthorn ee6692b607
Fix #2108: improve mode ignore docs 2026-07-22 23:12:03 +02:00
Oliver Blanthorn 958b369703
Fix #3732: arrows change completion if selected 2026-07-22 22:17:57 +02:00
Oliver Blanthorn 7e93e91940
Fix #281: treat dropdowns as texteditable 2026-07-22 22:15:02 +02:00
Oliver Blanthorn ff28b24da4
Fix #2089: do not use real text on mode indicator
This is a dumb hack - it's a fake select. But it works
2026-07-22 22:12:05 +02:00
Oliver Blanthorn a766f77cc4
Fix #3029: allow vimp hints to match space if unbound 2026-07-22 21:42:05 +02:00
Oliver Blanthorn d1d113586c
Fix #4213: add url2args tests 2026-07-22 21:15:10 +02:00
Oliver Blanthorn 7ea0dbc3ac
Fix #1817: set bmarkopen to exclude bmark from :open 2026-07-22 21:15:10 +02:00
Oliver Blanthorn d55f2665ff
Fix #2676: support leading digits in ids 2026-07-22 21:15:10 +02:00
Oliver Blanthorn fbcd7b6b2b
Fix #324: add more languages to followpage patterns
Happy to add even more if people want them
2026-07-22 21:15:10 +02:00
Oliver Blanthorn 8fc07143b2
Fix #1586: add :set themeprivacy true|false 2026-07-22 20:41:22 +02:00
Oliver Blanthorn 60f4016cc9
Fix #2645: update tab completions on tab change 2026-07-22 20:39:58 +02:00
Oliver Blanthorn 79d8bc79e2
Fix #3064: mention profile used for userChrome 2026-07-22 20:37:53 +02:00
Oliver Blanthorn 9b773435b3
Fix #2286: add find -i/-s flags for case sensitivity 2026-07-22 20:36:45 +02:00
Oliver Blanthorn f694159642
Fix #1169: add unkeymap 2026-07-22 20:33:29 +02:00
Oliver Blanthorn 4df421beb4
Fix #2100: w should not collapse selection in visual mode 2026-07-22 20:32:55 +02:00
Oliver Blanthorn 6b1376df37
Fix #3261: ;x/;X in frames 2026-07-22 20:25:15 +02:00
Oliver Blanthorn 34d41ec76c
Fix #3209: add gI bind for input selection 2026-07-22 20:23:44 +02:00
Oliver Blanthorn fd0351eba4
Fix #3228: improve docs about current binds 2026-07-22 19:44:39 +02:00
Oliver Blanthorn 2d28b57d0d
Fix #1957: make guiset hoverlink left explicit 2026-07-22 19:39:18 +02:00
Oliver Blanthorn 9ee9fcd931
Fix #5468: use theme variables on help page 2026-07-22 19:38:04 +02:00
Oliver Blanthorn 26d8ad194e
Fix #1378: do not require an RC file for custom themes 2026-07-22 19:34:53 +02:00
Oliver Blanthorn 958139740d
Fix #1658: disable counts in ignore and hint mode 2026-07-22 19:33:54 +02:00
Oliver Blanthorn ced0cce829
Fix #1370: add optional History autoselect 2026-07-22 19:31:35 +02:00
Oliver Blanthorn e60b8dc836
Switch to native highlights for find in FF140+ 2026-07-22 19:09:32 +02:00
Oliver Blanthorn b26ba76d50
Fix #516: add urlmodify --safe to prevent redirect spam 2026-07-22 17:47:34 +02:00
Oliver Blanthorn f912b3513b
Fix #675: add :abbreviate 2026-07-22 17:41:59 +02:00
Oliver Blanthorn 42c4ed27a3
Fix #997: add ModeEnter/ModeLeave autocmds 2026-07-22 17:39:29 +02:00
Oliver Blanthorn e83e71b401
Fix #2666: move :find highlights on resize 2026-07-22 17:04:30 +02:00
Oliver Blanthorn 0e640b05aa
Fix #5449: start empty configs at version 2.0 2026-07-22 16:11:33 +02:00
Oliver Blanthorn 0f6afd6cb4
Fix #3289: infer colourscheme name from url 2026-07-22 16:11:33 +02:00
Oliver Blanthorn 076e4940fc
Fix #5456: share tree formatting between :goto and :back 2026-07-22 16:11:33 +02:00
Oliver Blanthorn 8591d4c94a
Fix #5307: await customtheme before changing theme 2026-07-22 16:11:33 +02:00
Oliver Blanthorn f79b4db0d8
Fix #4763: do not save commands starting with space to history 2026-07-22 16:11:33 +02:00
Oliver Blanthorn 058fb34c7e
Fix #3129: send correct set message from help pages 2026-07-22 16:11:33 +02:00
Oliver Blanthorn 47dd838e11
Fix #4714: add true tabfirst/tablast commands 2026-07-22 16:11:32 +02:00
Oliver Blanthorn 68765720d0
Fix #3639: add gg/G smoothscroll 2026-07-22 15:33:07 +02:00
Oliver Blanthorn 2349904b1d
Fix #5279: restore hint names after reflow 2026-07-22 14:37:18 +02:00
Oliver Blanthorn 1668b18632
Fix #4492: make numeric prefixes optional in parser 2026-07-22 14:33:12 +02:00
Oliver Blanthorn 8a07ca307e
Fix #4311: respect pinned tabs during tabmove 2026-07-22 14:31:51 +02:00
Oliver Blanthorn b05a11d872
Make e2e tests actually exit 2026-07-22 14:17:49 +02:00
Oliver Blanthorn 14c7d77cd2
Fix #4605: allow autocmddelete on default autocmds 2026-07-22 13:23:43 +02:00
Oliver Blanthorn b487a712bd
Fix #5109: trigger ex input change on ex history navigation 2026-07-22 13:23:43 +02:00
Oliver Blanthorn 5b31cc42eb
Fix #2360: only process change if changed 2026-07-22 13:23:43 +02:00
Oliver Blanthorn 9ffb8f55ee
Fix #3361: support multiple pairs in urlmodify 2026-07-22 13:23:43 +02:00
Oliver Blanthorn 4c2be15b65
Fix #1151: use rendered text in contenteditable 2026-07-22 13:23:43 +02:00
Oliver Blanthorn 8eeee54e76
Merge pull request #5464 from tridactyl/fix_2761
Fix #2761: canonicalise aliases in completions
2026-07-22 08:41:59 +00:00
Oliver Blanthorn c4054c2050
Merge branch 'fix_3897' 2026-07-22 10:41:09 +02:00
Oliver Blanthorn 7d2c6322b3
Fix #3897: allow themes to style container borders 2026-07-22 10:41:00 +02:00
Oliver Blanthorn a578e0641f
Merge branch 'fix_5388' 2026-07-22 10:36:54 +02:00
Oliver Blanthorn 913ebe09c2
Fix #5388: throw on empty mode 2026-07-22 10:36:43 +02:00
Oliver Blanthorn bba7479c9c
Merge branch 'fix_3862' 2026-07-22 10:33:21 +02:00
Oliver Blanthorn 4e3787f064
Fix #3862: special case unseturl completions 2026-07-22 10:33:14 +02:00
Oliver Blanthorn 40b7db5060
Merge branch 'fix_3529' 2026-07-22 10:28:02 +02:00
Oliver Blanthorn f398b1e875
Fix #3529: remove hints on unfocus 2026-07-22 10:27:40 +02:00
Oliver Blanthorn cba51dfbd5
Merge branch 'fix_4772' 2026-07-22 10:25:44 +02:00
Oliver Blanthorn 9ac2f3f973
Fix #4772: special case tab for gobble 2026-07-22 10:25:37 +02:00
Oliver Blanthorn 1853b6ebaf
Merge branch 'fix_4917' 2026-07-22 10:22:09 +02:00
Oliver Blanthorn 77ebad6c2c
Fix #4917: follow autocontain proxy order 2026-07-22 10:21:57 +02:00
Oliver Blanthorn 8eb19e69e6
Fix #1757: return safely when perf obs disabled 2026-07-22 10:20:31 +02:00
Oliver Blanthorn eabaecc548
Fix #2761: canonicalise aliases in completions 2026-07-22 10:18:28 +02:00
Oliver Blanthorn 2200e7ba3f
Fix #2333: use real config.set for keytranslatemap 2026-07-22 00:08:42 +02:00
Oliver Blanthorn 53a137ad78
Merge branch 'fix_4352' 2026-07-21 23:55:27 +02:00
Oliver Blanthorn 206ada93fa
Fix #4352: make tri.l work on non-promises 2026-07-21 23:55:15 +02:00
Oliver Blanthorn 51b13deda6
Merge branch 'fix_4175' 2026-07-21 23:51:44 +02:00
Oliver Blanthorn 877bd0d5d5
Merge branch 'fix_2331' 2026-07-21 23:47:33 +02:00
Oliver Blanthorn ad315d7a8b
Fix #2331: source --url no longer shows fs 2026-07-21 23:47:23 +02:00
Oliver Blanthorn 13bddb2330
Fix #5085: make modeindicator invisible sooner 2026-07-21 23:45:14 +02:00
Oliver Blanthorn b5f2ab64c5
Fix #3673: add setnull completions 2026-07-21 22:46:22 +02:00
Oliver Blanthorn 8dd9beddbd
Fix #4175: don't ignore shift on space 2026-07-21 22:40:03 +02:00
Oliver Blanthorn 1d33a3278f
Merge branch 'fix_5147' 2026-07-21 22:27:18 +02:00
Oliver Blanthorn 5e97a9c9b1
Fix #5147: stop autoselection for tabdiscard --all 2026-07-21 22:27:11 +02:00
Oliver Blanthorn 51b9a6f8e2
Merge pull request #5462 from tridactyl/out_of_order_spaces
Fix out of order spaces
2026-07-21 20:01:06 +00:00
Oliver Blanthorn 454d9f0ff8
Merge branch 'fix_3868' 2026-07-21 21:53:38 +02:00
Oliver Blanthorn b2179417af
Fix #3868: only match %s1-9 as documented 2026-07-21 21:53:11 +02:00
Oliver Blanthorn 0e7b6e9437
Fix #3684: allow js filenames to contain -d
Five year wait for a one character patch. This is
the reason free and open source software powers the
entire world
2026-07-21 21:51:59 +02:00
Oliver Blanthorn ed8b0e9f05
Merge pull request #5461 from tridactyl/fix_5178
Fix #5178: filter ;p for unique text
2026-07-21 18:56:34 +00:00
Oliver Blanthorn e909093dd6
Fix #4723: skip percent-encoding in urlincrement 2026-07-21 20:55:33 +02:00
Oliver Blanthorn 3d1a1daf91
Fix #5451: make :bmarks support <C-o>w 2026-07-21 20:52:24 +02:00
Oliver Blanthorn 49d403b699
Clear commandline queue on close 2026-07-21 20:28:43 +02:00
Oliver Blanthorn 8374ae666d
Fix #5447: make space insertion native 2026-07-21 20:27:46 +02:00
Oliver Blanthorn 6f8ccff86f
Merge pull request #5460 from tridactyl/fix_4976
Ignore disabled buttons on default hint mode
2026-07-21 18:26:47 +00:00
Chic-Tweetz 3d9d536f2a
Fix #5178: filter ;p for unique text 2026-07-21 20:25:55 +02:00
Oliver Blanthorn 6dade7d1fb
Ignore disabled buttons on default hint mode
You can still click them if you use non-default hint modes
which I think is probably what users expect
2026-07-21 18:26:31 +02:00
Oliver Blanthorn 649a77962a
Fix #5459: use browserBg for management in completions 2026-07-21 17:41:49 +02:00
Oliver Blanthorn 346c13570e
Add <C-o>x bind to execute all completions
Fix #3133
2026-07-21 13:14:07 +02:00
Oliver Blanthorn 457154fae1
Merge branch 'got_mystery' 2026-07-21 09:54:33 +02:00
Oliver Blanthorn 8887423545
Ignore repeat keys when matching a prefix 2026-07-21 09:54:19 +02:00
Oliver Blanthorn 838e1519c9
Fix #5455: let optional keyups travel back in time 2026-07-21 09:23:49 +02:00
Oliver Blanthorn bc712e9327
Merge pull request #5454 from tridactyl/tabdetach_fallback
tabdetach fallback
2026-07-20 20:33:52 +00:00
Oliver Blanthorn 021cf44d00
Throw error on tabdetach in private window 2026-07-20 21:52:52 +02:00
Oliver Blanthorn 244b29c769
Improve tabdetach help 2026-07-20 21:52:24 +02:00
Oliver Blanthorn cc893cc264
Merge pull request #5453 from tridactyl/reader_sandbox
reader sandbox
2026-07-20 18:55:43 +00:00
Oliver Blanthorn 8d3ac6a503
Fix #5452: stop relying on CSS for instant scroll 2026-07-20 20:48:52 +02:00
Oliver Blanthorn d64c29689c
Support visual mode in iframes 2026-07-20 19:31:51 +02:00
Oliver Blanthorn 5857258a2b
Fix #4873: sandbox reader in an iframe
Introduces a regression: visual mode no longer works in
reader mode
2026-07-20 19:31:51 +02:00
Oliver Blanthorn d2bbb80892
Merge pull request #5381 from Chic-Tweetz/fix-iframe-listeners
Fix #1971: add listeners to same-origin iframes
2026-07-20 17:31:11 +00:00
Oliver Blanthorn 3cd207c93c
Give docs some TLC 2026-07-20 14:45:31 +02:00
Oliver Blanthorn b8e26f8d31
Fix website CSS/JS etc 2026-07-20 13:43:54 +02:00
Oliver Blanthorn 34fc0020a8
Hijack attachShadow to add iframe listeners 2026-07-20 13:23:30 +02:00
Oliver Blanthorn 5d63fe4edd
Improve same-origin iframe listening
1) watch for mutations and dedup observers

2) make keyup cancellation survive focus changes

3) add limited support for shadow roots too
2026-07-20 13:23:30 +02:00
Chic-Tweetz 0059a45670
tidy up lint errors from iframe listeners 2026-07-20 13:23:30 +02:00
Chic-Tweetz 84edddf07a
Fix #1971: add listeners to same-origin iframes 2026-07-20 13:23:30 +02:00
Oliver Blanthorn a4834ce2d5
Merge pull request #5450 from tridactyl/ts6_migration
ts6 migration
2026-07-20 11:22:36 +00:00
Oliver Blanthorn fcb55685d2
Update to ts6
Most of the changed lines in this are from random
breaking changes in typedoc meaning that lots of
docstrings needed to be rewritten
2026-07-20 00:24:35 +02:00
Oliver Blanthorn 15aee6cf3f
Migrate jest to v29 2026-07-19 19:17:44 +02:00
Oliver Blanthorn 944de159bd
Cheer up typedoc 2026-07-19 18:13:44 +02:00
Oliver Blanthorn 9edd57214c
Update to ts4.7 and bump typedoc 2026-07-19 18:03:22 +02:00
Oliver Blanthorn 894f4d78a6
Update to jest 27 2026-07-19 13:12:08 +02:00
Oliver Blanthorn 367db1302b
Make generated metadata deterministic
Ensure that files are built in the same order each time
2026-07-19 12:14:31 +02:00
Oliver Blanthorn 442d11336f
Preserve inferred object member types
Fix e.g. boolean/numeric types rather than stringifying
2026-07-19 12:14:05 +02:00
Oliver Blanthorn 8a4171e9b5
Resolve TypeDoc type aliases
Fix e.g. `:mode normal`
2026-07-19 12:12:22 +02:00
Oliver Blanthorn 187dcf69e3
Scope TypeDoc metadata by module
Prevent collisions between e.g. `get`, reduce filesize
2026-07-19 12:11:33 +02:00
Oliver Blanthorn 41ef1726f0
Cut filesize by 50% by removing newlines and spaces
...really
2026-07-19 11:09:47 +02:00
Oliver Blanthorn 0067efeccc
Cheer up linter 2026-07-19 11:09:47 +02:00
Oliver Blanthorn cd6ec5f37f
Fix minor typo 2026-07-19 11:09:47 +02:00
glacambre 2bd1f5502b
Re-implement metadata API on top of tsdoc output 2026-07-19 11:09:47 +02:00
Oliver Blanthorn 6cd9a1843f
Merge pull request #5445 from tridactyl/fix_4519
Fix #4519: decode URIs in lots of places
2026-07-18 14:03:41 +00:00
Oliver Blanthorn 0b55eaa1ec
Make get_current_url decode URI 2026-07-18 15:46:56 +02:00
Oliver Blanthorn cb76c5b070
Merge pull request #5446 from tridactyl/fix_lazy_css
Fix halloween + lazy iframe
2026-07-18 13:42:44 +00:00
Oliver Blanthorn dc63a6cb18
Fix #5444: don't lazy load lazy iframe 2026-07-18 15:21:33 +02:00
Oliver Blanthorn 51c79132a9
Hardcode esc/c-[ to cancel buffering 2026-07-18 15:19:28 +02:00
Oliver Blanthorn 77fc67ca0c
Fix #4226: document 'none' proxy 2026-07-18 14:56:37 +02:00
Oliver Blanthorn a5b8ab7e89
Fix #4519: decode URIs in lots of places 2026-07-18 14:12:40 +02:00
Oliver Blanthorn a4c318b416
Merge pull request #5443 from tridactyl/fix_5092
Fix #5092: update contentLocation on focus change
2026-07-18 11:40:07 +00:00
Oliver Blanthorn 82c2ca4859
Fix #5092: update contentLocation on focus change 2026-07-18 13:19:55 +02:00
Oliver Blanthorn c27332c3d5
Merge pull request #5442 from tridactyl/fix_worktrees
Fix builds in worktrees
2026-07-18 11:16:12 +00:00
Oliver Blanthorn d53d87d7fe
Merge pull request #5441 from tridactyl/fix_5003
Fix index calculation for hidden/pinned tabs
2026-07-18 10:33:21 +00:00
Oliver Blanthorn 47e2436ccf
Fix builds in worktrees
authors.html assumed there would always be a .git
directory, which is not true in worktrees
2026-07-18 12:22:32 +02:00
Oliver Blanthorn e7ad13dbca
Fix #5003: calculate indices when pinned/hidden 2026-07-18 12:17:34 +02:00
Oliver Blanthorn cac2c078e8
Merge pull request #5440 from tridactyl/fix_4958
Fix #4958: cancel the right keypress events
2026-07-18 10:02:47 +00:00
Oliver Blanthorn 9ec6688b31
Merge pull request #5439 from tridactyl/fix_4959
Fix #4954: stop using tabdetach hack for containers
2026-07-18 09:38:03 +00:00
Oliver Blanthorn 1ed1804d9b
Merge pull request #5438 from tridactyl/hint_popover_host
Fix #4954: move hints to popover
2026-07-18 09:37:18 +00:00
Oliver Blanthorn 59010c256f
Fix #4958: cancel the right keypress events 2026-07-18 11:33:20 +02:00
Oliver Blanthorn 0ef70c2a26
Fix #4959: stop using tabdetach hack for containers
Apparently possible since FF64? I don't really understand
why we were ever doing this
2026-07-18 11:27:48 +02:00
Oliver Blanthorn 6d50ecbda7
Fix #4954: move hints to popover 2026-07-18 01:01:38 +02:00
Oliver Blanthorn ad61b8c6a5
Merge pull request #5383 from tridactyl/no_more_cves_please
Add TrustedKeyboardEvent and compel usage
2026-07-17 18:06:14 +00:00
Oliver Blanthorn 85d7e8b81a
Fix nosmoothscrollwizard rebinds and add vim style aliases 2026-07-17 19:14:12 +02:00
Oliver Blanthorn fdb53c012e
Banish explicit any
This doesn't really actually help much
because the implicit anys are the really
dangerous ones but i can't face fixing
all those just now
2026-07-17 18:55:16 +02:00
Oliver Blanthorn 253bfc52bb
Add memoise helper 2026-07-17 18:18:18 +02:00
Oliver Blanthorn f7c870b045
Add TrustedKeyboardEvent and force/deny usage
TrustedKeyboardEvent MUST be used within Tridactyl to
ensure we do not accept events. It MUST NOT be used
within event listeners which MUST use runtime type
guards instead
2026-07-17 18:18:18 +02:00
Oliver Blanthorn cfbfbd511b
Merge pull request #5436 from Chic-Tweetz/buffer-fillcmdline-reliability
Improve `fillcmdline` key buffering
2026-07-17 16:02:48 +00:00
Oliver Blanthorn 5382257329
Buffer keys while iframe is loading 2026-07-17 10:50:10 +02:00
Chic-Tweetz 1d158cc124 Buffer keys for fillcmdline aliases and prevent commands starting with fillcmdline from buffering forever 2026-07-16 19:37:41 +01:00
Oliver Blanthorn f04f5cfa8a
Merge pull request #5431 from tridactyl/fix_storage_races
Fix storage races
2026-07-16 11:18:55 +00:00
Oliver Blanthorn 8b4dfc2c28
Support direct browser.storage access again
This adds an extra queue that in theory allows some compatibility
between our own config.* commands and browser.storage.*

But quickly doing direct storage writes and config.set will still
cause weird things to happen
2026-07-16 13:02:21 +02:00
Oliver Blanthorn 64d6e1949a
Suffer from hubris: delete help message about #1409 2026-07-16 13:02:21 +02:00
Oliver Blanthorn b742bbeab5
Swap icky stringification for a proper clone 2026-07-16 13:02:20 +02:00
Oliver Blanthorn e921cb901e
Stop using browser storage events to update config
Should fix #1409
2026-07-16 13:01:28 +02:00
Oliver Blanthorn f771106b65
Merge pull request #5423 from tridactyl/fillcmdline_yes_wait
WIP: add --wait flag to fillcmdline
2026-07-13 14:09:57 +00:00
Oliver Blanthorn 45f2e0e157
WIP: add --wait flag to fillcmdline 2026-07-13 15:51:05 +02:00
Oliver Blanthorn 76fd349a76
Merge pull request #5430 from tridactyl/lazy_iframe
Fix #639: load iframe lazily
2026-07-13 13:48:48 +00:00
Oliver Blanthorn 2766a2c6f2
Merge pull request #5434 from Chic-Tweetz/visibility-intersection-observer
Use `IntersectionObserver` to check element visibility
2026-07-13 13:48:07 +00:00
Oliver Blanthorn d959d654ab
Catch errors in hint construction
e.g. `:hint -c [`
2026-07-13 14:47:04 +02:00
Oliver Blanthorn cb4725e596
Reuse visible elems logic for JS hints 2026-07-13 14:42:49 +02:00
Chic-Tweetz 397cfca593 Use getVisibleElemsBySelector for hinting, requiring some sync to async 2026-07-12 14:43:24 +01:00
Chic-Tweetz 048f6d09e4 Add getVisibleElemsBySelector which uses IntersectionObservers to filter visible elems 2026-07-12 14:25:24 +01:00
Oliver Blanthorn ac017eb7f6
Merge pull request #5432 from tridactyl/per_platform_lint_rules
Some checks failed
lint / lint (unit) (push) Has been cancelled
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Per platform lint rules
2026-07-11 17:07:16 +00:00
Oliver Blanthorn c127fb6bea
Turn unsupported-apis into a rule factory
This way we can mark specific platforms as compatible
2026-07-11 15:02:07 +02:00
Oliver Blanthorn 8d18f541bc
Generate manifest to share min version 2026-07-11 14:51:40 +02:00
Oliver Blanthorn 410eb34528
Fix #639: load iframe lazily 2026-07-10 16:45:03 +02:00
Oliver Blanthorn a696038929
Fix #5248: warn on U/D browser bind
Some checks are pending
e2e / test (firefoxesr, ubuntu) (push) Waiting to run
e2e / test (firefox, ubuntu) (push) Waiting to run
e2e / test (firefox, windows) (push) Waiting to run
lint / lint (lint) (push) Waiting to run
lint / lint (mozilla) (push) Waiting to run
lint / lint (unit) (push) Waiting to run
Website / build (push) Waiting to run
Website / deploy (push) Blocked by required conditions
2026-07-10 14:53:44 +02:00
Chic-Tweetz c37db0e6d6 Use scrollduration config as :scrollstart speed divisor 2026-07-09 20:23:50 +01:00
Chic-Tweetz afbf5d214f suggest using :smoothscrollwizard in :scrollstart help 2026-07-09 20:08:31 +01:00
Oliver Blanthorn 000ccf551d
Add smooth scroll binding wizard 2026-07-09 20:03:29 +02:00
Chic-Tweetz d2883e599b Rename :smoothscrollstart/stop to :scrollstart/stop 2026-07-09 18:11:03 +01:00
paris 20023db656 Add :smoothscroll and :smoothscrollstop for keydown/keyup binds 2026-07-06 01:04:35 +01:00
Oliver Blanthorn 9d767bb018
Merge pull request #5426 from tridactyl/rssexec_pls
Some checks failed
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Try to make rssexec test less flakey
2026-07-05 15:00:27 +00:00
Oliver Blanthorn d23d3ae461
Merge pull request #1645 from tridactyl/pushykeyseq
keyup / keydown binding
2026-07-05 14:41:57 +00:00
Oliver Blanthorn abea927768
Merge pull request #5425 from tridactyl/youtube_music_memleak_fix
Cap size of js hints and do some GC
2026-07-05 14:41:04 +00:00
Oliver Blanthorn debea430a0
Merge pull request #5424 from tridactyl/completion_memleak_fix
Stop accumulating tab listeners, objects forever
2026-07-05 14:40:47 +00:00
Oliver Blanthorn f3e6e961bc
Try to make rssexec test less flakey 2026-07-05 16:39:36 +02:00
Oliver Blanthorn 052f3c3f33
Cap size of js hints and do some GC
Should fix #5230
2026-07-05 14:22:49 +02:00
Oliver Blanthorn a6a010b286
Stop accumulating tab listeners, objects forever
I'm hoping this fixes #1302
2026-07-05 14:19:33 +02:00
Oliver Blanthorn c27c2d48c8
Add unit test to ensure untrusted keys are rejected 2026-07-05 13:10:41 +02:00
Oliver Blanthorn fac4885cc7
Improve docs for legacy modes 2026-07-05 12:17:16 +02:00
Oliver Blanthorn 468223cb39
Stop mutating vanilla keyevent 2026-07-05 12:15:54 +02:00
Oliver Blanthorn 93123ee5ed
Make sure keypress events aren't cancelled twice 2026-07-05 11:38:35 +02:00
Oliver Blanthorn 6f848ea768
Extract queue management from cancelling
This stops us from having to cancel keyups twice
2026-07-05 11:35:07 +02:00
Oliver Blanthorn 8b2f71df6b
Only buffer keydowns for commandline 2026-07-05 11:33:48 +02:00
Oliver Blanthorn 91c8097800
Document gholk's ignore layer 2026-07-05 11:10:25 +02:00
Oliver Blanthorn 063a1e3874
Merge branch 'master' into pushykeyseq 2026-07-05 11:05:50 +02:00
Oliver Blanthorn 386efca4ef
Merge pull request #5422 from tridactyl/fillcmdline_tmp_no_wait
Some checks are pending
e2e / test (firefox, ubuntu) (push) Waiting to run
e2e / test (firefox, windows) (push) Waiting to run
e2e / test (firefoxesr, ubuntu) (push) Waiting to run
lint / lint (lint) (push) Waiting to run
lint / lint (mozilla) (push) Waiting to run
lint / lint (unit) (push) Waiting to run
Website / build (push) Waiting to run
Website / deploy (push) Blocked by required conditions
Fix #5402: make fillcmdline_tmp return early
2026-07-04 13:33:23 +00:00
Oliver Blanthorn 93020300a6
Fix #5402: make fillcmdline_tmp return early 2026-07-04 15:04:08 +02:00
Oliver Blanthorn 363f4fb3b7
Cheer up linter 2026-07-04 13:41:48 +02:00
Oliver Blanthorn a3bea8b0bc
Don't create containers when looking for proxies 2026-07-04 13:40:31 +02:00
Oliver Blanthorn 75d231b7c1
Fix #5290: multiple container race condition 2026-07-04 13:40:01 +02:00
Oliver Blanthorn 36b0efcb7a
Canonicalise URL before matching proxies/containers 2026-07-04 13:28:18 +02:00
Oliver Blanthorn 006540ba49
Fix #5335: document about:keyboard for binding to reserved keys 2026-07-04 11:44:47 +02:00
Oliver Blanthorn 7c84a9a520
Fix #5415: update updatenag update text 2026-07-04 11:41:08 +02:00
Oliver Blanthorn cb2fbc3fd2
Fix #5130: tiny typo 2026-07-04 11:38:41 +02:00
Oliver Blanthorn d7bd210e07
Fix #5359: update editor-adapter 2026-07-04 11:37:48 +02:00
Oliver Blanthorn 1c394454b5
Fix #5144: add new postcss dep to hugo build
Some checks are pending
e2e / test (firefox, ubuntu) (push) Waiting to run
e2e / test (firefox, windows) (push) Waiting to run
e2e / test (firefoxesr, ubuntu) (push) Waiting to run
lint / lint (lint) (push) Waiting to run
lint / lint (mozilla) (push) Waiting to run
lint / lint (unit) (push) Waiting to run
Website / build (push) Waiting to run
Website / deploy (push) Blocked by required conditions
Also needed to fix upstream newletter date issue in tridactyl/site
2026-07-04 11:21:43 +02:00
Oliver Blanthorn 185fe1f2de
Do some code golf 2026-06-30 15:26:28 +02:00
Oliver Blanthorn 7d228c72a3
Ignore mergiraf orig files 2026-06-30 14:39:59 +02:00
Oliver Blanthorn f2c5b1ebde
Merge branch 'master' into pushykeyseq 2026-06-30 14:27:38 +02:00
Oliver Blanthorn e603a3eb24
Fix broken examples in docstring 2026-06-30 14:23:32 +02:00
Oliver Blanthorn 4dfacf9bf4
Fix modeindicatorshowkeys
U/D annotations are only shown if matching
bind has them explicitly
2026-06-30 14:09:23 +02:00
Oliver Blanthorn 09811efbe3
Fix default nmode binds 2026-06-30 13:22:51 +02:00
Oliver Blanthorn 8897ded643
Warn when keydown binds are shadowed
Main fix is changing from a string match to a check of
what the mapstrs actually parse to
2026-06-30 13:16:04 +02:00
Oliver Blanthorn f34b7d08cd
Fix binds greedily matching when there are modifiers 2026-06-30 13:02:48 +02:00
Oliver Blanthorn 29fa4b8ba0
Merge pull request #5412 from Chic-Tweetz/fix-closed-shadow-doms
Some checks are pending
e2e / test (firefox, ubuntu) (push) Waiting to run
e2e / test (firefox, windows) (push) Waiting to run
lint / lint (lint) (push) Waiting to run
lint / lint (mozilla) (push) Waiting to run
Website / build (push) Waiting to run
e2e / test (firefoxesr, ubuntu) (push) Waiting to run
lint / lint (unit) (push) Waiting to run
Website / deploy (push) Blocked by required conditions
Let insert mode work for input elements in closed shadow DOMs
2026-06-28 11:41:31 +00:00
Chic-Tweetz 6f8202d5ee
fix lint error 2026-06-28 13:35:22 +02:00
Chic-Tweetz 718b8a142d
cast Element to HTMLElement for linter 2026-06-28 13:35:22 +02:00
Chic-Tweetz d21ad1f029
replace all instances of .shadowRoot with .openOrClosedShadowRoot 2026-06-28 13:35:15 +02:00
Oliver Blanthorn 811022bf7b
Merge pull request #5413 from Chic-Tweetz/fix-insubstantial-first-focusinput
prevent insubstantial LAST_USED_INPUT on page focus
2026-06-28 10:57:20 +00:00
Chic-Tweetz b8c8c4aebe prevent insubstantial LAST_USED_INPUT on page focus 2026-06-21 14:30:52 +01:00
Oliver Blanthorn 2922ded7bc
Add visual mode bind # for text fragment yank
Fix #5407
2026-06-19 19:07:52 +02:00
Oliver Blanthorn 40aba31190
Cheer up linter 2026-06-19 18:29:09 +02:00
Oliver Blanthorn 1fa04aaa2f
Fix <C-,>d bind 2026-06-19 18:28:02 +02:00
Oliver Blanthorn a9eb82e49f
Fix tests 2026-06-19 18:26:52 +02:00
Chic-Tweetz cc2694131e allow keyfeed keybinds 2026-06-19 13:10:50 +01:00
Oliver Blanthorn fcf30bae40
Reapply "Merge pull request #5389 from GHolk/unbind-default"
This reverts commit 9a31a79ae8.

Fix #5401
2026-06-17 15:50:47 +02:00
Oliver Blanthorn 254097cd3f
Fix jsonview (viewconfig etc) in FF152+ 2026-06-17 15:48:46 +02:00
Oliver Blanthorn 81a0df7f54
release 1.24.6 2026-06-17 11:20:18 +02:00
Oliver Blanthorn 0a91af6a7f
Update changelog for 1.24.6
:(
2026-06-17 11:20:12 +02:00
Oliver Blanthorn 03e230cc5a
Update changelog for 1.25.6 2026-06-17 11:16:58 +02:00
Oliver Blanthorn 9a31a79ae8
Revert "Merge pull request #5389 from GHolk/unbind-default"
This reverts commit 50aae4a898, reversing
changes made to b006c219d9.

Temporarily reverting so we can safely rush out the hotfix for #5368
2026-06-17 10:54:22 +02:00
Oliver Blanthorn 808317f3d2
Cheer up linter 2026-06-17 10:47:38 +02:00
Oliver Blanthorn eddfe2b41a
Fix #5368: work around nerfed tabs.insertCSS 2026-06-17 10:46:50 +02:00
gholk 1febc15c16
Fix bracket-expr + single char binds 2026-06-15 16:37:18 +02:00
Oliver Blanthorn 4bf6032a08
Fix explicit <D-j> matching repeat keys 2026-06-10 15:21:07 +02:00
Oliver Blanthorn 91356c9ce4
Fix regression with repeat keys not firing 2026-06-10 14:18:29 +02:00
Oliver Blanthorn f6a527fe84
Cheer up linter 2026-06-10 13:06:40 +02:00
Oliver Blanthorn 256a8fa72a
Fix hint mode 2026-06-10 13:06:17 +02:00
Oliver Blanthorn dbed660872
Fix <Space> binds
```
Number(' ') === 0
```

js needs to be nuked from orbit
2026-06-10 12:54:12 +02:00
Oliver Blanthorn ecdb3338fe
Fix keyup events triggering, update tests 2026-06-10 12:37:53 +02:00
Oliver Blanthorn 24b012e666
Merge branch 'master' into pushykeyseq 2026-06-10 12:36:59 +02:00
Oliver Blanthorn 5cddc889fa
Move prettier to linter rather than pre-commit 2026-06-06 13:44:24 +02:00
Oliver Blanthorn 06457ee187
Merge pull request #5392 from Chic-Tweetz/fix-scrolling-jitter
Fix display dpi-dependent scroll jitter
2026-06-03 13:47:37 +00:00
Oliver Blanthorn 50aae4a898
Merge pull request #5389 from GHolk/unbind-default
fix: unbind default and default config overwritten by mergeDeep
2026-06-03 13:38:34 +00:00
Oliver Blanthorn b006c219d9
Merge pull request #5394 from Chic-Tweetz/fix-finding-hang
Fix find hanging when ranges are inaccessible
2026-05-30 09:46:53 +00:00
Chic-Tweetz 6b7c346169 Fix find hanging when ranges are inaccessible 2026-05-27 20:58:53 +01:00
Chic-Tweetz 37bf4cb3e5 Fix display dpi-dependent scroll jitter 2026-05-26 12:08:48 +01:00
gholk 4d1e672bf1 fix: config.get does not merge site and user config if default undefined 2026-05-25 22:51:40 +08:00
gholk e5619e8ef1 fix: ensure a nake object without built-in method
and remove non-necessary prototype chain
2026-05-25 22:50:58 +08:00
Oliver Blanthorn d1f51136fd
Merge pull request #5265 from matystroia/master
More Consistent Status Indicator Classes
2026-05-22 13:24:03 +00:00
Matei Stroia 9739c7df86
Merge branch 'tridactyl:master' into master 2026-05-22 13:27:57 +03:00
gholk 98b92d562a fix: the unbind key get clear and reset to default after mergeDeepCull
The mergeDeepCull set a unbind key (null) to undefined.
In get and getURL, there are multiple merge operations,
and undefined is ignored in the following merge.

We still need to remove null because null cause problem in keymap.
In most situation, we access the config with `tri.config.get` method,
so remove removeNull at return statement in get method is enough.
2026-05-20 23:20:02 +08:00
Oliver Blanthorn f3a43e14c1
Add funding.json for floss.fund 2026-05-17 13:43:47 +02:00
Oliver Blanthorn c02669a8f5
Merge pull request #5385 from GHolk/master
fix: js -d delimeter in arguments and visualenterauto in insert-mode
2026-05-16 16:23:52 +00:00
gholk 0c69ad2f22 fix: cancel the ending key event in gobble mode too
wihtout this, `gobble <Space> fillcmdline`
will send the space key to the web page
(and trigger the scroll event)
2026-05-16 20:11:36 +08:00
gholk 858f2ee15d feat: expose hintState object 2026-05-16 19:31:36 +08:00
gholk ff9daffd83 feat: smart transpose char before and after newline
In emacs, transpose chars at beginning or end of line
work like beginning or end of string.
2026-04-27 23:53:29 +08:00
gholk 345f1dbef5 fix: visualenterauto not work in insert mode #5306
* Do not enter visual mode in ex, ignore, hint mode
* Enter visual mode if text selected and the text is not in
  the content-editable node.
2026-04-27 23:15:00 +08:00
gholk fb00b01981 fix: Allow js -d accept arguments contain delimeter
Make following command work:

    command alert-args js -d@ alert(JS_ARGS.join('&')) @
    alert-args user root@localhost

`s.split(separator)[1]` will only take the second part
and ignore the following parts.
2026-04-27 20:31:38 +08:00
Oliver Blanthorn 007d93a181
Remove trailing space 2026-04-25 13:35:54 +02:00
Oliver Blanthorn fdff6d999f
Ooops 2026-04-25 12:55:16 +02:00
Oliver Blanthorn 3d558b5f38
Add keyfeed --page for feeding events to pages 2026-04-25 12:49:07 +02:00
Oliver Blanthorn ee8a2f6aeb
Add dev loop
NB: if you change excmd arguments, this is a footgun
because it doesn't run the python script
2026-04-25 12:47:06 +02:00
Oliver Blanthorn 3a67cc805f
Fix #5377: add agents.md to encourage reviewable PRs 2026-04-20 12:25:26 +02:00
Oliver Blanthorn 48baeb91f6
Merge pull request #5374 from tridactyl/cache_fuse
Cache fuse and stop direct array access
2026-04-18 14:52:01 +00:00
Oliver Blanthorn 028f72ae9e
Cheer up linter 2026-04-16 14:13:40 +02:00
repparw 2b6cab5e13
Cache fuse and stop direct array access 2026-04-16 13:23:30 +02:00
repparw c0263c2976
Improve performance of rapid hinting reset (4ms -> 0ms) 2026-04-16 12:52:19 +02:00
Oliver Blanthorn 0f0aaef006
Merge branch 'optimize-completion-dom-updates' 2026-04-16 12:35:00 +02:00
Oliver Blanthorn b8d343ede8
Merge pull request #5340 from repparw/optimize-completion-dom-updates
Optimize completion display DOM updates
2026-04-16 10:34:34 +00:00
Oliver Blanthorn 159d81fdca
Switch to replaceChildren 2026-04-16 12:33:30 +02:00
Oliver Blanthorn 9cac02f2f9
Merge pull request #5366 from Arjan-P/fix-selection-across-node
Fix selection across DOM node
2026-04-16 07:46:36 +00:00
Oliver Blanthorn 9074d0355a
Fix #904: make blacklistkeys more reliable 2026-04-10 15:17:55 +02:00
Arjan 22d3cf73d9 Add modify as a property on Selection type 2026-04-03 01:54:09 +05:30
Arjan ae67820ddf Fix selection across DOM node 2026-03-31 22:51:04 +05:30
Oliver Blanthorn af488f66d0
Merge pull request #5362 from Arjan-P/guard-vmaps-lh-selection-collapse
Added a guard to prevent selection collapse using h, l motion in visu…
2026-03-24 17:41:59 +00:00
Arjan 11bec3f1dd
Prevent selection collapse in visual mode 2026-03-24 18:36:05 +01:00
Oliver Blanthorn cbcc92694c
Cheer up prettier 2026-03-19 16:11:53 +01:00
Oliver Blanthorn 5cc7312882
Fork cleanslate to fix #5323 2026-03-13 17:32:35 +01:00
wskozel 38163d0e1a fix(tabdetach): harden detach window creation and accept symbolic tab indexes 2026-03-05 20:18:08 +00:00
Oliver Blanthorn e4e5c56802
Fix indentation error
How did 'let's make almost invisible differences syntactically
important' ever become popular instead of just some cruel joke
2026-02-24 16:09:48 +01:00
Oliver Blanthorn 1eeab1f980
Add yamllint 2026-02-24 16:09:21 +01:00
Oliver Blanthorn 9cd67eee4c
release 1.24.5 2026-02-24 15:53:16 +01:00
Oliver Blanthorn a328821a12
Update changelog for 1.24.5
... the new git hook kind of mangled this a bit so that's nice
2026-02-24 15:51:40 +01:00
Oliver Blanthorn 4fb30adfdb
Fix website upload, maybe, take 3 2026-02-21 18:04:06 +01:00
Oliver Blanthorn 12cb96e14e
Fix website upload, maybe, take 2 2026-02-21 17:53:23 +01:00
Oliver Blanthorn 67b893ca6c
Fix website upload, maybe 2026-02-21 17:43:00 +01:00
Oliver Blanthorn 8596302451
Merge pull request #5344 from repparw/fix/flaky-e2e-test
fix: add explicit wait for window handles in e2e test setup
2026-02-21 16:13:39 +00:00
repparw ffa4902da9
fix: add 500ms sleep as additional buffer for extension init 2026-02-21 17:08:35 +01:00
repparw f75c90ee23
fix: wait for iframe element after window switch 2026-02-21 17:08:35 +01:00
repparw 0831811699
perf: add build caching and remove unnecessary sleep in e2e setup 2026-02-21 17:08:30 +01:00
Oliver Blanthorn 38631348db
Merge pull request #5345 from tridactyl/fix_hooks
Fix git hooks
2026-02-21 16:01:54 +00:00
Oliver Blanthorn e61f883661
Fix git hooks 2026-02-19 22:31:15 +01:00
repparw 1b74a2641c fix: add explicit wait for window handles in e2e test setup
The getAllWindowHandles() call was racing with Firefox extension
loading in headless mode, causing 'InvalidArgumentError: missing
field handle' errors. Add explicit wait for 2+ handles before
accessing them.
2026-02-17 16:41:25 -03:00
repparw ade0e50e68 Fix: use innerHTML+appendChild instead of replaceChildren for TS compatibility 2026-02-16 16:25:56 -03:00
repparw 80c38680ff Optimize completion display: use DocumentFragment instead of cloneNode 2026-02-16 15:58:53 -03:00
Oliver Blanthorn 6162b13809
Update changelog for 1.24.5 2026-02-16 13:00:24 +01:00
Oliver Blanthorn dc18d58edc
Merge pull request #5235 from tridactyl/better-webrequest-autocmds
Better webrequest autocmds
2026-02-14 11:38:56 +00:00
Oliver Blanthorn a05eda5a8b
Remove commented out code 2026-02-14 11:18:11 +00:00
Oliver Blanthorn d2aa586833
Merge pull request #5276 from Chic-Tweetz/hint-element-overlays
Enhance theming with hint element overlays
2026-02-14 11:16:31 +00:00
Oliver Blanthorn bddedf3b0c
Merge pull request #4712 from tridactyl/tabnext_nowrap
Fix #4704: add --nowrap flag to tabnext/prev
2026-02-14 11:14:03 +00:00
Oliver Blanthorn b186f5f4a6
Merge pull request #4879 from debnath-d/patch-1
Improve readability of fonts in midnight.css
2026-02-14 11:12:50 +00:00
Oliver Blanthorn 96e03c14cb
Fix gi on GitHub in RC 2026-02-12 13:32:57 +01:00
Oliver Blanthorn 405ee11d2f
Merge pull request #5334 from Chic-Tweetz/fix-hintables-losing-clientRects
Check hintable elements' clientRects still exist when building hints
2026-01-28 11:40:18 +00:00
Chic-Tweetz 4e26f5fbe7 set all hints' hidden field false after ensuring their elements' clientRects still exist 2026-01-27 15:11:30 +00:00
Oliver Blanthorn 02dfd6b78d
Merge pull request #5331 from Chic-Tweetz/fix-cmdline-buffer
Buffer keys for the commandline for fillcmdline with args
2026-01-23 13:52:59 +00:00
Chic-Tweetz e08e1a272f buffer keys for fillcmdline excmds with args 2026-01-17 13:20:24 +00:00
Oliver Blanthorn 1280ad682b
Merge pull request #5321 from bennyyip/fix/profile-win
fix: profilerun doesn't work on windows
2025-12-15 18:40:26 +00:00
bennyyip c6563992a7 fix: profilerun doesn't work on windows
sv
2025-12-16 02:10:49 +08:00
Oliver Blanthorn 1b0d6d3297
Revert to traditional spelling of 'Release' 2025-12-14 20:25:01 +01:00
Oliver Blanthorn 4b6e75b4f1
Add WIP changelog for 1.24.5 2025-12-14 20:08:33 +01:00
Oliver Blanthorn 566c4870b9
Merge branch 'winter2425' 2025-12-09 19:22:06 +01:00
Oliver Blanthorn 9c8e3fe0eb
Release winter 2024 newsletter. Oops. 2025-12-09 19:21:11 +01:00
Oliver Blanthorn 69c3d825a4
Fix #5319: Update permission docs 2025-12-09 19:07:26 +01:00
Oliver Blanthorn 5c644595ba
Merge pull request #5315 from Chic-Tweetz/fix-cmdline-transparency
Allow transparency on cmdline iframe on dark sites
2025-12-09 17:22:52 +00:00
Oliver Blanthorn e78b927df6
Merge pull request #5318 from tridactyl/google_less_ai
Add magic udm=14 param to Google
2025-12-09 17:21:56 +00:00
Oliver Blanthorn 6b36607d5e
Add magic udm=14 param to Google
At the time of writing, this hides the terrible AI overviews
and some other stuff, just like selecting the 'Web' tab
2025-12-08 21:21:54 +01:00
Chic-Tweetz 2ea75f90f5 transparent iframe on dark sites 2025-12-06 17:26:31 +00:00
Oliver Blanthorn 687789c623
Merge pull request #5304 from tridactyl/saveas-filename-only
Saveas filename only
2025-12-03 16:36:48 +00:00
Oliver Blanthorn 4b27bd52af
Improve help 2025-12-03 17:36:19 +01:00
glacambre e868f1b10f
Fix #3333: make binds show up in help 2025-12-03 14:04:29 +01:00
Oliver Blanthorn 86de01e1a7
Fix #3053: support counts for gi 2025-12-03 14:01:51 +01:00
Oliver Blanthorn 70494c9596
Fix help help text formatting 2025-12-03 13:41:09 +01:00
Oliver Blanthorn 0486ed7b70
Fix #1972: open help in background tab, new window etc 2025-12-03 13:40:28 +01:00
Oliver Blanthorn 09bb55924b
Refactor :help to use arg.lib 2025-12-03 13:34:30 +01:00
Oliver Blanthorn bcdfa98298
Bikeshed download filename placeholder 2025-12-03 13:03:03 +01:00
Babil G. Sarwar 7563fd9e28
Fix save-as download success message 2025-12-03 12:53:37 +01:00
Babil G. Sarwar 44f23f14e7
Add configurable save-as filename marker support 2025-12-03 12:53:29 +01:00
Oliver Blanthorn 749dd59903
Fix #5303: Stop styling commandline
Other third party themes should follow this recipe using
nesting and a :not
2025-12-03 12:51:04 +01:00
Oliver Blanthorn 22ab6b4f65
Add class to commandline to make it easier to not style 2025-12-03 12:49:21 +01:00
Oliver Blanthorn 4d196fd88f
Try to fix theme race condition
Does rather beg the question why we use rc.fetchText at all
2025-12-01 13:45:01 +01:00
Oliver Blanthorn 1606193bd6
Merge pull request #5293 from ztstroud/feat/warn-tabonly-args
Improve user experience with tabopen and tabonly
2025-11-30 16:39:34 +00:00
Oliver Blanthorn 25bb2764cb
Add sidescrolling newspaper reader theme to contrib/ 2025-11-30 17:39:07 +01:00
Oliver Blanthorn 0cd0e505fe
Bikeshed :tabo deprecation message 2025-11-30 17:11:58 +01:00
Zak Stroud b2e6bc62a6 refactor: remove reference to tabopen from tabonly error message
The error made sense when tabo was a built in alias and could be
confused with tabopen. With tabo removed, the warning is still useful
but the mention in the error does not.
2025-11-24 20:20:30 -08:00
Zak Stroud 30ae6d7a79 feat: remove the tabo alias
Since tabonly can be so destructive, remove it entirely with a note
about how to add it back if someone needs it.
2025-11-24 20:18:02 -08:00
Oliver Blanthorn 8f87fbef3d
Merge pull request #5297 from tridactyl/reader_big_pages
Fix reader on big pages
2025-11-24 16:59:22 +00:00
Oliver Blanthorn faeec5384d
Add reader_articles type info to cheer up TS 2025-11-24 17:55:36 +01:00
Oliver Blanthorn 2c700182f8
Bake magic string to cheer up TS 2025-11-24 17:55:33 +01:00
Oliver Blanthorn 6f85b38a69
Fall back to URL in private windows 2025-11-24 13:10:13 +01:00
Oliver Blanthorn 521d43f1fa
Fix dumb backwards compat bug 2025-11-24 13:09:43 +01:00
Oliver Blanthorn d88fa4e975
Cheer up linter 2025-11-24 13:00:16 +01:00
Oliver Blanthorn ab5e5c4649
Tidy up local storage on read 2025-11-23 20:30:30 +01:00
Oliver Blanthorn c15f0c9003
Fix #5291: use local storage to store :reader pages 2025-11-23 20:24:36 +01:00
Oliver Blanthorn 3f9a19243f
Add uuidv4 to math
Ideally we'd use the built-in crypto stuff but ... it's only available
on HTTPS pages. really.
2025-11-23 20:17:00 +01:00
Oliver Blanthorn a4ab5c5932
Merge pull request #5296 from tridactyl/reader_themes
Reader themes
2025-11-23 19:16:09 +00:00
Oliver Blanthorn c47960ca6e
Add official seturl support for themes 2025-11-23 19:44:13 +01:00
Oliver Blanthorn 6124ab93f4
Use gholk's arg fork for :colourscheme 2025-11-23 18:07:09 +01:00
Zak Stroud 42a55663b8 feat: warn users if they run tabonly with arguments
tabonly can be destructive. The tabo alias looks like it could be for
tabopen, and running tabonly with arguments still closed all other tabs.

Fixes: #5239
2025-11-22 15:51:26 -08:00
Oliver Blanthorn 030ef4d2ab
Merge pull request #5283 from Chic-Tweetz/reader-favicons
favicon in reader
2025-11-15 16:28:21 +00:00
Oliver Blanthorn 90e437cc35
Fix #5287: add Liberapay account 2025-11-15 17:20:56 +01:00
repparw ba93521570
Fix #5284: support nonstandard shortlink on youtube 2025-11-14 17:13:38 +01:00
Chic-Tweetz b85f619290 use original page favicon in reader 2025-10-17 11:33:39 +01:00
Chic-Tweetz 1e88b17533 allow different hint-fg if overlays are used 2025-10-13 18:36:53 +01:00
Chic-Tweetz 8e01db94ab improved tokyonight readability with default hintstyles 2025-10-13 15:07:41 +01:00
Chic-Tweetz 652eb493f3 vimium theme: improved readability & centred cmdln 2025-10-12 18:00:03 +01:00
Chic-Tweetz 239f4248cd tokyonight: consistent css var names, removed personal preference rules 2025-10-11 18:16:04 +01:00
Chic-Tweetz 33cc24ea0f removed unused parameter 2025-10-11 18:03:49 +01:00
Chic-Tweetz e0eac6ec7f scroll handler: requestAnimationFrame (fixes bbc.co.uk sign in popup) 2025-10-11 14:27:00 +01:00
Chic-Tweetz 2820c94a50
moved Vimperator hint char generation out of loop 2025-10-11 14:20:03 +02:00
Chic-Tweetz 83d11d4846
Moved char generation out of loop 2025-10-11 14:20:03 +02:00
Chic-Tweetz 24fcb22cc2
fix js hint chars shadowing
Instead of iterating over Hintables objects in HintPage(), pass the array of Hintables to the HintBuilder and loop over them in there.
Changed HintBuilder param type from Hintables to Hintables[] for this.
2025-10-11 14:19:58 +02:00
Chic-Tweetz 8909457a16 hintstylesoverlays: fg to none, tidier rect adding 2025-10-05 13:40:07 +01:00
Chic-Tweetz 2398626335 added tokyonight theme 2025-10-03 11:25:17 +01:00
Chic-Tweetz ce50461ec6 missed an important to make hint overlays appear 2025-10-02 19:24:41 +01:00
Chic-Tweetz bc954dc33c removed a big comment, always remove scroll listener on reset 2025-10-02 18:54:50 +01:00
Chic-Tweetz 46a7d8edf7 vimium theme, hintstyles.overlay rules, helper excmds to set hinstyle rules quickly 2025-10-02 18:45:43 +01:00
Chic-Tweetz c59770c981 optional hint overlays + active hintspan rules 2025-10-02 17:02:32 +01:00
Oliver Blanthorn c97d3f35e8
Merge pull request #5274 from Chic-Tweetz/focus-after-tabdetach
Focus after tabdetach
2025-09-30 18:30:18 +00:00
Chic-Tweetz 636df7ae3e :tabdetach workaround to give new window focus 2025-09-26 20:29:59 +01:00
Chic-Tweetz 0bc38cb546 focus page on :tabdetach 2025-09-26 17:42:17 +01:00
Oliver Blanthorn 6fb98288a4
Merge pull request #5270 from Chic-Tweetz/contenteditable-leniency
allow for empty or plaintext-only contenteditable values
2025-09-24 16:03:27 +00:00
Chic-Tweetz 6ed23de314 allow for empty or plaintext-only contenteditable values 2025-09-23 13:39:25 +01:00
Oliver Blanthorn 89225aeebe
Merge pull request #5269 from bennyyip/master
Allow XHTML pages
2025-09-23 11:54:44 +00:00
bennyyip 4e01b741b5 Allow XHTML pages 2025-09-23 18:40:21 +08:00
Matei Stroia 834c7755d1 make status indicator class change atomic 2025-09-15 16:38:41 +03:00
Oliver Blanthorn bc6d051591
Merge pull request #5249 from lawrencetheabhorrence/unbind-recursive
`:unbind --recursive`
2025-09-13 13:07:04 +00:00
Oliver Blanthorn 199db429bc
Update docstring for --recursive 2025-09-13 15:04:53 +02:00
Julyanna Francesca C. Huang 1dafb76912
error message when --recursive is called on bind 2025-09-13 14:53:23 +02:00
Julyanna Francesca C. Huang 1b860da584
recursive unbind 2025-09-13 14:53:23 +02:00
Julyanna Francesca C. Huang a9de0897b9
parse_bind_args checks for flags with '--' prefix 2025-09-13 14:53:23 +02:00
Oliver Blanthorn 5039e1f687
Merge pull request #5250 from boriselec/issue-3499
fix docs: data URI is not supported
2025-09-13 12:35:48 +00:00
Oliver Blanthorn dc675ceff2
Merge pull request #5252 from rlch/profiles
feat: Support creating, launching and renaming profiles
2025-09-13 12:26:22 +00:00
RJM 8a18322c5f
fix: Make docs more terse; use logger properly 2025-09-13 14:14:25 +02:00
RJM 8f60f430c0
feat: Support creating, launching and renaming profiles 2025-09-13 14:14:24 +02:00
Oliver Blanthorn 0972873803
Merge pull request #5253 from Chic-Tweetz/single-char-hintspans
Reset hint char styles when rapidly selecting hints
2025-09-13 12:10:07 +00:00
Oliver Blanthorn 5b278be67e
Merge pull request #5260 from Chic-Tweetz/cmdline-check-isconnected
Reinsert the iframe if it's removed before document.readyState "complete"
2025-09-13 12:09:30 +00:00
Oliver Blanthorn c0b902fb8d
Merge pull request #5262 from Chic-Tweetz/hint-nested-shadows
Hint nested & closed shadows
2025-09-13 12:09:15 +00:00
Oliver Blanthorn e150da80ba
Cheer up linter 2025-09-13 13:54:36 +02:00
Oliver Blanthorn 934aa8c4cd
Merge pull request #5263 from WyattJordan/fix-typo
mode not moode, typo
2025-09-13 11:45:53 +00:00
Wyatt Jordan 5aa667c4cf mode not moode, typo 2025-09-11 17:30:34 -04:00
Chic-Tweetz ceac48fc13 always add listener, moved to content.ts, readd status indicator 2025-09-10 13:27:43 +01:00
Chic-Tweetz f57849c2db removed pretty change to match main branch 2025-09-09 19:16:38 +01:00
Chic-Tweetz c9969cdd11 fix: window -> document.addEventListener 2025-09-09 19:08:05 +01:00
Chic-Tweetz ccc77b583c added some cmdline existence checks 2025-09-09 17:47:45 +01:00
Chic-Tweetz 2a407f87f9 getShadowElementsBySelector now checks nested shadow roots & closed shadows 2025-09-08 20:14:48 +01:00
Oliver Blanthorn e1cce5f2b9
Fix History setting field 2025-09-01 13:30:09 +02:00
Chic-Tweetz 555a0d6179 reset styles when rapidly selecting hints 2025-08-20 16:37:24 +01:00
boriselec b6d6ed88fe fix docs: data URI is not supported
issues/3499
2025-08-17 19:40:56 +03:00
Oliver Blanthorn 0885811abe
Document tabopen --discard 2025-08-05 11:14:46 +02:00
Oliver Blanthorn 6b8cf13056
release 1.24.4 2025-08-04 18:50:08 +02:00
Oliver Blanthorn 46d1eba5e0
Update changelog for 1.24.4 2025-08-04 18:48:21 +02:00
Oliver Blanthorn 7d54aad581
Merge pull request #5244 from oldshensheep/fix-smooth-scrolling
Fix smooth scrolling in Firefox 141
2025-08-01 13:41:31 +00:00
oldshensheep 6e3d17906e Fix smooth scrolling in Firefox 141 2025-07-30 16:10:10 +08:00
oldshensheep 8e7557d648 Revert "Disable smooth scroll in FF141+ until we can fix it #5240"
This reverts commit c5d4329274.
2025-07-30 16:04:11 +08:00
Oliver Blanthorn 344fc9fad7
release 1.24.3 2025-07-25 15:31:57 +02:00
Oliver Blanthorn 63b27d8c7b
Update changelog for 1.24.3 2025-07-25 15:30:53 +02:00
Oliver Blanthorn 681ccc9023
Mention smoothscroll on newtab and ask for money :( 2025-07-25 15:05:04 +02:00
Oliver Blanthorn c5d4329274
Disable smooth scroll in FF141+ until we can fix it #5240 2025-07-25 14:55:57 +02:00
Julyanna Francesca C. Huang f49ca25703
Add packageManager to package.json for corepack 2025-07-20 18:51:58 +02:00
Julyanna Francesca C. Huang 11d6f1f509
Add tridactylrc location to :mkt 2025-07-20 18:50:53 +02:00
Oliver Blanthorn fe57d2c263
Cheer up linter
For future reference it now needs to be run with

```
yarn run eslint --rulesdir custom-eslint-rules --fix [the file you want to fix]
```
2025-07-03 20:16:47 +02:00
Oliver Blanthorn f1d827a784
skill issue 2025-07-03 19:08:35 +01:00
Colin Caine 40a157f8cc Update docs for autocmds 2025-06-25 17:13:00 +02:00
Colin Caine d5e7f7b570 Make webrequest autocmds less annoying to use
This commit makes it so:

- adding an aucmd for a webrequest event will display an error to the
  user if it fails to add one
- old aucmds for the same pattern will be removed correctly
- trying to remove a non-existent aucmd will not fail
2025-06-25 16:47:16 +02:00
Oliver Blanthorn 50f19a34fc
Merge branch 'cassepipe/master' 2025-06-10 17:04:07 +02:00
Oliver Blanthorn a6caa97352
Make preferHTML configurable 2025-06-10 17:03:37 +02:00
Oliver Blanthorn db60e42943
Merge pull request #5231 from thatsmydoing/inert
Make iframe inert when not visible
2025-06-10 14:44:18 +00:00
Thomas Dy e35f3716d3 Make iframe inert when not visible
This prevents it from being accidentally focused when it's not meant to
be.
2025-06-10 21:04:51 +09:00
Oliver Blanthorn ea3666365d
Merge pull request #5226 from Chic-Tweetz/single-char-hintspans
Allow styling typed letters separately when hinting
2025-06-04 14:24:55 +00:00
Oliver Blanthorn d3bdc9a898
Squashed commit of bookmark-folders #5220
commit a4b1bd96e5
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Sat May 17 07:52:36 2025 +0300

    removed unused parameter

commit 35a3403be7
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Sat May 17 07:35:15 2025 +0300

    added get active source for state

commit ca98667af7
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Sat May 17 07:23:38 2025 +0300

    returned getActiveCompletion def

commit 1fe3190c6b
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Sat May 17 07:05:22 2025 +0300

    cleaned up changes in cmd_frame

commit 357b6285da
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Sat May 17 07:03:53 2025 +0300

    cleaned up changes in cmd_frame

commit 51ff3c8091
Merge: e8823894 5270e1ef
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Sat May 17 06:53:27 2025 +0300

    single folder deep suggestions

commit e882389427
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Fri Apr 11 19:42:10 2025 +0300

    single folder deep suggestions

commit 6b0975eceb
Merge: a7c57da1 f9e3e558
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Fri Apr 11 18:19:48 2025 +0300

    Merge branch 'original-master' into develop

commit a7c57da1f1
Merge: f3c34d22 961726bf
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Mon Mar 3 18:54:56 2025 +0200

    Merge branch 'original-master' into develop

commit f3c34d22ba
Merge: 7e636e4a 83e62116
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Mon Mar 3 16:45:38 2025 +0200

    Merge branch 'tstmove' into develop

commit 7e636e4ab0
Merge: ca0f5166 d270d5e3
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Tue Feb 25 16:51:21 2025 +0200

    Merge branch 'tstmove' into develop

commit ca0f516613
Merge: 203a90df 0f580048
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Tue Feb 25 16:31:39 2025 +0200

    Merge branch 'tstmove' into develop

commit 203a90df89
Merge: 535c0d22 cef6f101
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Fri Feb 7 13:21:13 2025 +0200

    Merge branch 'bookmark-folder-completions' into develop

commit 535c0d22a7
Merge: f1156d5c 729f0648
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Fri Feb 7 13:21:02 2025 +0200

    Merge branch 'bookmark-folders' into develop

commit cef6f101ca
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Tue Jan 21 16:30:09 2025 +0200

    bkmark formatting

commit 7ff2420a57
Author: Vsevolod Chernetskyi <vchernetskyi993@gmail.com>
Date:   Tue Jan 21 16:29:35 2025 +0200

    added search by folder in all bookmarks
2025-06-04 16:17:46 +02:00
Chic-Tweetz f25b9658ef
unchanged lets to consts 2025-05-27 12:08:41 +01:00
Chic-Tweetz f9df0688db
keep typed chars same colour by default 2025-05-27 11:41:57 +01:00
paris fbda0a2970 style hint chars as they're typed 2025-05-26 22:23:20 +01:00
Oliver Blanthorn 2fe6825b37
Merge pull request #5222 from bew/js-d-trim-filename
Trim filename before source when using `js` or `jsb`
2025-05-20 16:44:33 +00:00
Oliver Blanthorn 3fce68da15
Merge pull request #5221 from bew/js-jsb-docs-improvements
Improve docs for js & jsb
2025-05-20 16:43:57 +00:00
Oliver Blanthorn ebe5117e69
Change recommended :js delim 2025-05-20 17:43:46 +01:00
Benoit de Chezelles 3af370cf1a Trim filename before source when using js or jsb 2025-05-18 13:03:29 +02:00
Benoit de Chezelles 94eb591033 Mention about:debugging instead of the Browser Console with everything 2025-05-18 12:05:08 +02:00
Benoit de Chezelles 4b4a9f41ce Improve docs for js & jsb 2025-05-18 11:44:55 +02:00
Oliver Blanthorn 5270e1ef53
Merge pull request #5145 from vchernetskyi993/master
Added path prefixes for bmarks
2025-05-14 09:50:32 +00:00
Oliver Blanthorn 7310194afd
Merge pull request #5214 from tridactyl/draft_failfast
Try to fail faster on draft PRs
2025-05-05 09:03:30 +00:00
Oliver Blanthorn 5c2ba72206
Try to fail faster on draft PRs 2025-05-05 10:58:37 +02:00
Oliver Blanthorn 0c66ad0365
Merge pull request #5213 from FugueSoft/fugue-edit
excmd help edit
2025-04-25 18:35:08 +00:00
Oliver Blanthorn 13258b5483
Clarify editorcmd help 2025-04-25 20:34:15 +02:00
FugueSoft 8c18d372b2 Typographical update. 2025-04-25 11:17:18 -05:00
FugueSoft 19190d3118 Added a few lines to editorcmd description for kitty nvim users 2025-04-25 11:04:22 -05:00
Oliver Blanthorn 3c41724497
Remove extension from md file in clippy to fix the link 2025-04-21 18:08:42 +02:00
Oliver Blanthorn f9e3e5585a
Merge pull request #5205 from eleanor-clifford/master
Fix simulateClick for <a>s inside <svg>s
2025-03-25 14:44:59 +00:00
Oliver Blanthorn 964b83c328
Fix #5204: quickmarks now make gp[key] private binds 2025-03-25 15:39:46 +01:00
Eleanor Clifford bf9f5fd193 Fix simulateClick for <a>s inside <svg>s 2025-03-24 19:16:43 +00:00
Oliver Blanthorn 1b674d637d
Merge pull request #5195 from vchernetskyi993/tabmove-previous
tabmove - added branch for previous tab index
2025-03-15 08:57:35 +00:00
Oliver Blanthorn be35470616
Merge pull request #5198 from tridactyl/cmc-document-config-test
Explain what the config.test.ts test does
2025-03-15 08:51:51 +00:00
Colin Caine b4be583cfc Explain what the config.test.ts test does 2025-03-14 14:32:00 +00:00
Vsevolod Chernetskyi cf912fbad7 tabmove - added branch for previous tab index 2025-03-11 19:43:07 +02:00
Oliver Blanthorn 02a5731d88
Merge pull request #5193 from tridactyl/cmc-fix-typo
Some checks failed
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
fix typo
2025-03-11 13:45:49 +00:00
Colin Caine 01f18bf213 fix typo 2025-03-11 13:18:31 +00:00
Oliver Blanthorn 60118c49ec
Update :sidebaropen help
Some checks failed
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
2025-03-09 19:07:59 +01:00
Oliver Blanthorn dd6a0a8893
Finish drafting 'winter' 24/25 newsletter 2025-03-09 19:07:31 +01:00
Oliver Blanthorn cc0dccda32
Add WIP newsletter 2025-03-09 17:39:11 +01:00
Oliver Blanthorn e14eae6898
Merge pull request #5037 from michael2to3/fix/donottouchxml
Prevent Non-HTML Content-Type inject iframe
2025-03-09 11:43:42 +00:00
Oliver Blanthorn 616efd681e
Merge branch 'master' into fix/donottouchxml 2025-03-09 12:35:08 +01:00
Oliver Blanthorn 2e8f21380c
Merge pull request #5187 from GHolk/tab-history-undefined-error
Some checks are pending
e2e / test (firefox, ubuntu) (push) Waiting to run
e2e / test (firefox, windows) (push) Waiting to run
e2e / test (firefoxesr, ubuntu) (push) Waiting to run
lint / lint (lint) (push) Waiting to run
lint / lint (mozilla) (push) Waiting to run
lint / lint (unit) (push) Waiting to run
Website / build (push) Waiting to run
Website / deploy (push) Blocked by required conditions
Fix TabHistory completion error message
2025-03-08 10:26:06 +00:00
gholk 09c9d9a3fd Fix TabHistory completion error message
Sometimes TabValue history is undefined
and flattenTree(undefined) will throw an error.
The error will be caught but we can do more elegant.
2025-03-08 13:11:36 +08:00
Oliver Blanthorn 961726bfbd
Merge pull request #5181 from vchernetskyi993/tstmove
Some checks failed
lint / lint (unit) (push) Has been cancelled
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Added ability to move TST trees
2025-03-03 16:48:51 +00:00
Oliver Blanthorn 7d4765a7ad
Improve tst help messages 2025-03-03 17:48:15 +01:00
Vsevolod Chernetskyi 83e6211687 removed number conversions 2025-03-03 16:28:45 +02:00
Vsevolod Chernetskyi 937ab820c9 reverted ternary formatting 2025-03-03 16:21:45 +02:00
Vsevolod Chernetskyi 9efffe78a5 reverted ternary formatting 2025-03-03 16:20:21 +02:00
Vsevolod Chernetskyi d1a6551f36 reworked tst commands to accept indices 2025-03-03 16:17:51 +02:00
Vsevolod Chernetskyi d270d5e3dd docs and naming fix 2025-02-25 16:50:53 +02:00
Vsevolod Chernetskyi 8df96a5995 reverted ternary formatting 2025-02-25 16:43:00 +02:00
Vsevolod Chernetskyi 0f58004816 refactored tst specifics into separate class 2025-02-25 16:29:52 +02:00
Vsevolod Chernetskyi 9e6b18cf2f current tab in current window only 2025-02-24 19:53:00 +02:00
Vsevolod Chernetskyi 254c7edcf7 added tstattach 2025-02-24 19:48:09 +02:00
Vsevolod Chernetskyi bb7150d998 added tstmovetree command 2025-02-21 19:08:27 +02:00
Oliver Blanthorn f1156d5c72
Fix #5155: add commandlineterriblewebsitefix setting to workaround server-side vandalism
Some checks failed
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
I am not going to add detection for every bazillion ways web frameworks
can break Tridactyl, so let's add a setting so that people who insist
on visiting bad websites can still do so
2025-02-01 21:54:41 +01:00
Chic-Tweetz 4157002ff3
Fix #5152: stop overriding completion font size
Some checks failed
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Also change default completion font size to ensure that this doesn't
change any theme font sizes
2025-01-21 12:58:59 +01:00
Vsevolod Chernetskyi 729f064855 copied fuse options from completions fuse 2025-01-19 16:18:11 +02:00
Vsevolod Chernetskyi 0fda7cd274 reworded valid bookmark function doc 2025-01-18 13:40:34 +02:00
Vsevolod Chernetskyi ca2165d57c code cleanup 2025-01-18 11:16:40 +02:00
Vsevolod Chernetskyi 49928b37e4 added path prefix to history and bookmarks 2025-01-18 10:57:02 +02:00
Vsevolod Chernetskyi 659e6e84bb property to enable bookmark folders 2025-01-17 18:28:46 +02:00
Vsevolod Chernetskyi 9a9bd5e5d3 removed root___ magic constant 2025-01-17 17:58:21 +02:00
Vsevolod Chernetskyi 2d97f9ffad removed unused allBookmarks function 2025-01-16 19:47:49 +02:00
Vsevolod Chernetskyi 95efb18f15 added search by folder in all bookmarks 2025-01-16 19:40:21 +02:00
Vsevolod Chernetskyi 6cad579065 4721 - added path prefixes for bmarks 2025-01-15 20:50:44 +02:00
Oliver Blanthorn 72be7ed9aa
Fix #5143: skip iframe check if iframe will never load
Some checks failed
lint / lint (unit) (push) Has been cancelled
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
2025-01-15 13:46:48 +01:00
Oliver Blanthorn c7e59eba10
Merge pull request #5142 from Xorger/patch-1
Some checks failed
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Add `:open` note to readme.md
2025-01-14 08:17:36 +00:00
Elijah Yajaman 9910b7288c
Add :open note to readme.md
Add a note explaining how to set the newtab page for `:open`
2025-01-14 14:58:40 +10:00
Oliver Blanthorn cf4baae9cf
Merge pull request #5138 from bew/patch-1
Some checks failed
lint / lint (unit) (push) Has been cancelled
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Mention `unbindurl` can work with `bind` keys as well
2025-01-11 13:23:42 +00:00
Benoit de Chezelles b7272fb378
Mention unbindurl can work with bind keys as well 2025-01-11 13:24:06 +01:00
Oliver Blanthorn 4ec9254111
Merge pull request #5117 from tridactyl/dependabot/npm_and_yarn/eslint-plugin-jsdoc-50.6.1
Some checks failed
lint / lint (lint) (push) Has been cancelled
lint / lint (mozilla) (push) Has been cancelled
lint / lint (unit) (push) Has been cancelled
e2e / test (firefox, ubuntu) (push) Has been cancelled
e2e / test (firefox, windows) (push) Has been cancelled
e2e / test (firefoxesr, ubuntu) (push) Has been cancelled
Website / build (push) Has been cancelled
Website / deploy (push) Has been cancelled
Bump eslint-plugin-jsdoc from 48.2.1 to 50.6.1
2024-12-17 10:37:45 +00:00
Oliver Blanthorn dad52e9b12
Merge pull request #5116 from tridactyl/dependabot/npm_and_yarn/geckodriver-5.0.0
Bump geckodriver from 4.3.3 to 5.0.0
2024-12-17 10:37:16 +00:00
Oliver Blanthorn 5ba1f94608
Merge pull request #5115 from tridactyl/dependabot/npm_and_yarn/prettier-3.4.2
Bump prettier from 3.2.5 to 3.4.2
2024-12-17 10:37:02 +00:00
Oliver Blanthorn f68a157b9d
Merge pull request #5114 from tridactyl/dependabot/npm_and_yarn/command-line-args-6.0.1
Bump command-line-args from 5.2.1 to 6.0.1
2024-12-17 10:36:34 +00:00
Oliver Blanthorn 94272d2c4f
Merge pull request #5113 from tridactyl/dependabot/npm_and_yarn/jest-webextension-mock-4.0.0
Bump jest-webextension-mock from 3.9.0 to 4.0.0
2024-12-17 10:36:09 +00:00
Oliver Blanthorn 276fffe0ef
Merge pull request #5111 from tridactyl/dependabot/npm_and_yarn/types/css-0.0.38
Bump @types/css from 0.0.37 to 0.0.38
2024-12-17 10:35:57 +00:00
dependabot[bot] 4fcc6d3072
Bump eslint-plugin-jsdoc from 48.2.1 to 50.6.1
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 48.2.1 to 50.6.1.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v48.2.1...v50.6.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 22:17:11 +00:00
dependabot[bot] 7a68826987
Bump geckodriver from 4.3.3 to 5.0.0
Bumps [geckodriver](https://github.com/webdriverio-community/node-geckodriver) from 4.3.3 to 5.0.0.
- [Release notes](https://github.com/webdriverio-community/node-geckodriver/releases)
- [Changelog](https://github.com/webdriverio-community/node-geckodriver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webdriverio-community/node-geckodriver/compare/v4.3.3...v5.0.0)

---
updated-dependencies:
- dependency-name: geckodriver
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 22:16:34 +00:00
dependabot[bot] e8eaca07d6
Bump prettier from 3.2.5 to 3.4.2
Bumps [prettier](https://github.com/prettier/prettier) from 3.2.5 to 3.4.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.2.5...3.4.2)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 22:16:15 +00:00
dependabot[bot] c7b633dba9
Bump command-line-args from 5.2.1 to 6.0.1
Bumps [command-line-args](https://github.com/75lb/command-line-args) from 5.2.1 to 6.0.1.
- [Release notes](https://github.com/75lb/command-line-args/releases)
- [Commits](https://github.com/75lb/command-line-args/compare/v5.2.1...v6.0.1)

---
updated-dependencies:
- dependency-name: command-line-args
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 22:16:07 +00:00
dependabot[bot] c240917cff
Bump jest-webextension-mock from 3.9.0 to 4.0.0
Bumps [jest-webextension-mock](https://github.com/RickyMarou/jest-webextension-mock) from 3.9.0 to 4.0.0.
- [Changelog](https://github.com/RickyMarou/jest-webextension-mock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/RickyMarou/jest-webextension-mock/commits)

---
updated-dependencies:
- dependency-name: jest-webextension-mock
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 22:15:52 +00:00
dependabot[bot] 92ce6e2f81
Bump @types/css from 0.0.37 to 0.0.38
Bumps [@types/css](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/css) from 0.0.37 to 0.0.38.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/css)

---
updated-dependencies:
- dependency-name: "@types/css"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 22:15:18 +00:00
cassepipe 193e2eb090
Change preferHtml now defaults to false 2024-12-15 15:50:43 +01:00
Oliver Blanthorn 457eaf2443
Bike shed logging message 2024-09-09 10:34:09 +02:00
Mikhail Gaile 15685b7ddc
fix: prevent inj iframe if ct include xml 2024-09-07 16:22:45 +03:00
Mikhail Gaile a4dc22b919
fix: prevent inj iframe in wrong content-type 2024-09-01 22:00:43 +03:00
D. Debnath 57abfc5123
Improve readability of fonts in midnight.css
Font weight of 200 makes it hard to read. Default font weight makes it easier to read.
2023-12-26 14:49:24 +05:30
Oliver Blanthorn f50181ba04
Fix radix 2023-06-05 16:01:21 +02:00
Oliver Blanthorn a3be85fc72
Fix #4704: add --nowrap flag to tabnext/prev 2023-05-23 18:29:18 +02:00
Oliver Blanthorn 71c1f5f92b
Fix typo 2020-06-02 18:30:20 +01:00
Oliver Blanthorn e232bd3de9
Comment out failing keyseq test 2020-06-02 17:48:11 +01:00
Oliver Blanthorn 7ff1b79bb2
Update more keyseq tests for keyup binds 2020-06-02 17:47:48 +01:00
Oliver Blanthorn a0c38cc1d9
Fix counts in keyseq
The tests found a real bug. Are they supposed to do that?
2020-06-02 17:35:17 +01:00
Oliver Blanthorn cfdeaed925
Minor refactor 2020-06-02 17:22:30 +01:00
Oliver Blanthorn e00679966f
Update some keyseq tests 2020-06-02 17:18:36 +01:00
Oliver Blanthorn af90d4cec2
Document keyup, keydown and optional binds 2020-05-31 23:22:49 +01:00
Oliver Blanthorn 10c3a1a727
Fix special keys not working in command line 2020-05-31 22:37:50 +01:00
Oliver Blanthorn 38c5eeb16e
Add implicit keyup bind to more complex keys 2020-05-31 22:15:36 +01:00
Oliver Blanthorn 3af7b1f05d
Add optional keyup binds to all simple non-final keys in bound keysequences
E.g. `gg` -> `g<?U-g>`
2020-05-31 21:26:40 +01:00
Oliver Blanthorn 3046814c8d
Fix daft bug with optional keys 2020-05-31 21:09:18 +01:00
Oliver Blanthorn bd4107a6ea
Add R annotation for explicit may-be-repeat
NB: this is actually a placebo, only D binds cannot be repeat firings.
The purpose of this is to allow us to convert bare keys - e.g. g - to
<R-g><?U-g> at parse-time and to allow people to disallow keyup by
explicitly binding to <R-g>.
2020-05-31 21:09:18 +01:00
Oliver Blanthorn 6b5865aafd
Remove unused declaration 2020-05-31 21:09:18 +01:00
Oliver Blanthorn 34db87492b
Add annotation for optional key 2020-05-31 21:09:18 +01:00
Oliver Blanthorn d0c935ac45
Lazy fix for hint/gobble mode 2020-05-31 21:09:17 +01:00
Oliver Blanthorn 5fbb1b0cf1
Add explicit non-repeat keydown bindings 2020-05-31 21:09:17 +01:00
Oliver Blanthorn 046c014893
Add working keyup parsing
NB: this breaks special keys e.g. `<Enter>`
2020-05-31 21:09:17 +01:00
Oliver Blanthorn 5bb88b1bb3
WIP keyup binding 2020-05-31 21:09:13 +01:00
212 changed files with 14952 additions and 7840 deletions

View file

@ -1,3 +1,5 @@
const browserTargets = require("./browser-targets.json")
/*
👋 Hi! This file was autogenerated by tslint-to-eslint-config.
https://github.com/typescript-eslint/tslint-to-eslint-config
@ -30,11 +32,17 @@ module.exports = {
},
"plugins": [
"@typescript-eslint",
"@typescript-eslint/tslint",
"sonarjs"
],
"rules": {
"unsupported-apis": "warn",
"unsupported-apis-chrome": [
"warn",
{ "minimumVersion": browserTargets.chrome.minimumVersion }
],
"unsupported-apis-firefox": [
"warn",
{ "minimumVersion": browserTargets.firefox.minimumVersion }
],
"sonarjs/cognitive-complexity": "off", //"error",
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-unused-collection": "off", //"error", // There seems to be a bug with this rule - exported collections are assumed unused
@ -42,32 +50,16 @@ module.exports = {
"@typescript-eslint/array-type": "off",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"@typescript-eslint/ban-types": [
"@typescript-eslint/no-restricted-types": [
"error",
{
"types": {
"Object": {
"message": "Avoid using the `Object` type. Did you mean `object`?"
},
"Function": {
"message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`."
},
"Boolean": {
"message": "Avoid using the `Boolean` type. Did you mean `boolean`?"
},
"Number": {
"message": "Avoid using the `Number` type. Did you mean `number`?"
},
"String": {
"message": "Avoid using the `String` type. Did you mean `string`?"
},
"Symbol": {
"message": "Avoid using the `Symbol` type. Did you mean `symbol`?"
"KeyboardEvent": {
"message": "Use `TrustedKeyboardEvent` to prevent remote code injection from hostile pages."
}
}
}
],
"@typescript-eslint/class-name-casing": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off", // this should be "error" but the fix silently breaks code almost 100% of the time. not worth the headaches
@ -78,25 +70,14 @@ module.exports = {
}
],
"@typescript-eslint/explicit-module-boundary-types": "off", //"warn", // This is another hard one to enable
"@typescript-eslint/indent": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": [
"off",
{
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
"@typescript-eslint/member-ordering": [
"error",
{ "default": ["field", "constructor", "method"] },
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-array-delete": "off",
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-base-to-string": "off",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-floating-promises": "off", //"error", // We should turn this on eventually but it will take a while to fix
@ -112,10 +93,11 @@ module.exports = {
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-redundant-type-constituents": "off",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unsafe-assignment": "off", //"error",
"@typescript-eslint/no-unsafe-argument": "off", //"error",
"@typescript-eslint/no-unsafe-call": "off", //"error",
"@typescript-eslint/no-unsafe-member-access": "off", //"error", // We've done this a lot, but it would be a good idea to fix it
"@typescript-eslint/no-unsafe-return": "off", //"error", // We've done this a lot, but it would be a good idea to fix it
@ -126,21 +108,24 @@ module.exports = {
"allowTernary": true,
}
],
"@typescript-eslint/no-unused-vars-experimental": [
"@typescript-eslint/no-unused-vars": [
"error",
{
"ignoreArgsIfArgsAfterAreUsed": true,
"args": "after-used",
"argsIgnorePattern": "^_",
"caughtErrors": "none",
"varsIgnorePattern": "^_",
},
],
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/only-throw-error": "off",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/prefer-promise-reject-errors": "off",
"@typescript-eslint/prefer-regexp-exec": "error",
"@typescript-eslint/quotes": [
"quotes": [
"error",
"double",
{
@ -151,10 +136,6 @@ module.exports = {
"@typescript-eslint/require-await": "error",
"@typescript-eslint/restrict-plus-operands": "off", //"error", // We use this a lot - fixing it is a problem for a rainy day
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/semi": [
"off",
null
],
"@typescript-eslint/triple-slash-reference": [
"error",
{
@ -163,7 +144,6 @@ module.exports = {
"lib": "always"
}
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
@ -191,7 +171,6 @@ module.exports = {
"import/order": "off",
"jsdoc/check-alignment": "off",
"jsdoc/check-indentation": "off",
"jsdoc/newline-after-description": "off",
"max-classes-per-file": "off",
"max-len": "off",
"new-parens": "error",
@ -255,6 +234,12 @@ module.exports = {
"valid-typeof": "off"
},
"overrides": [
{
"files": ["*.d.ts"],
"rules": {
"@typescript-eslint/no-unused-vars": "off",
},
},
{
"files": ["src/completions/*.ts", "src/excmds.ts"],
"rules": {
@ -270,5 +255,24 @@ module.exports = {
"@typescript-eslint/prefer-regexp-exec": "off",
},
},
{
"files": ["src/content.ts", "src/commandline_frame.ts"],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-restricted-types": [
"error",
{
"types": {
"TrustedKeyboardEvent": {
"message": "Events must be validated with `isTrustedKeyboardEvent` at runtime"
},
"KeyboardEvent": {
"message": "Use `Event` instead"
},
},
},
],
},
}
],
};

1
.github/FUNDING.yml vendored
View file

@ -2,5 +2,6 @@
# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: bovine3dom
liberapay: bovine3dom
patreon: tridactyl
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY

View file

@ -26,10 +26,12 @@ jobs:
matrix:
os: [ubuntu, windows]
# os: [ubuntu, macos, windows]
browser: [firefox, firefoxesr]
browser: ${{ fromJSON(github.event_name == 'schedule' && '["latest", "latest-esr", "latest-beta"]' || '["latest", "latest-esr"]') }}
exclude:
- os: windows
browser: firefoxesr
browser: latest-esr
- os: windows
browser: latest-beta
runs-on: ${{ matrix.os }}-latest
@ -42,12 +44,23 @@ jobs:
cache: 'yarn'
- name: Install deps
run: yarn install
run: yarn install --frozen-lockfile
- name: Setup Firefox
uses: browser-actions/setup-firefox@v1
with:
firefox-version: ${{ matrix.browser == 'firefox' && 'latest' || 'latest-esr' }}
firefox-version: ${{ matrix.browser }}
- name: Cache build outputs
uses: actions/cache@v4
with:
path: |
build
dist
out
key: build-${{ matrix.os }}-${{ matrix.browser }}-${{ hashFiles('yarn.lock', 'package.json', 'src/**/*') }}
restore-keys: |
build-${{ matrix.os }}-${{ matrix.browser }}-
- name: Print Firefox version (Unix-like)
if: matrix.os == 'ubuntu' || matrix.os == 'macos'
@ -55,12 +68,10 @@ jobs:
- name: Build and test (Firefox)
uses: nick-fields/retry@v3
env:
HEADLESS: 1
with:
max_attempts: ${{ github.event_name == 'pull_request' && 5 || 10 }}
max_attempts: ${{ github.event_name == 'pull_request' && ( github.event.pull_request.draft && 1 || 5 ) || 10 }}
timeout_minutes: 10
retry_wait_seconds: 10
shell: bash
command: |
yarn run clean && yarn run build --old-native && yarn make-zip && yarn jest
yarn run build --old-native && yarn make-zip && yarn jest

View file

@ -12,6 +12,19 @@ on:
- '/readme.md'
jobs:
commit-emails:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check commits for noreply addresses
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: sh ./ci/check-commit-emails.sh
lint:
runs-on: ubuntu-latest
strategy:
@ -20,8 +33,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'yarn'
- name: Setup
run: yarn install
run: yarn install --frozen-lockfile
- name: ${{ matrix.step }}
env:
STEP: ${{ matrix.step }}

View file

@ -28,17 +28,35 @@ jobs:
repository: 'tridactyl/site'
submodules: 'recursive' # we shouldn't need this, public should be generated each time
path: 'site'
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: 'latest'
extended: true
- name: Setup PostCSS
run: |
yarn global add postcss postcss-cli autoprefixer
echo "$(yarn global bin)" >> "$GITHUB_PATH"
- name: Build
run: |
cd tridactyl
yarn install
yarn install --frozen-lockfile
yarn run build
find . -iname "*.html" -exec sed 's@href="/static@href="/build/static@' -i '{}' ';' # ideally this url would be less gnarly
# The hosted copy cannot run WebExtension scripts and lives under /build.
find build -iname "*.html" -exec sed -E -i \
-e 's@="/static@="/build/static@g' \
-e 's@<script src="/(content|help)\.js"></script>@@g' '{}' +
cd ../site
sudo apt-get install -y hugo
hugo
cp -r ../tridactyl/build/ public
- uses: actions/upload-pages-artifact@v1
- uses: actions/upload-pages-artifact@v4
with:
path: 'site/public'
retention-days: 2
@ -55,4 +73,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4

4
.gitignore vendored
View file

@ -10,10 +10,10 @@ native/native_main
native_main.spec
.wine-pyinstaller
tags
compiler/*.js
compiler/**/*.js
.*.generated.ts
.*.generated.json
.tmp/
.DS_Store
.build_cache/
yarn-error.log
*.orig

View file

@ -34,6 +34,19 @@
" " get it in the browser, or just restart.
"
" " If you're bovine3dom run sed 's|^" ||' .tridactylrc > ~/.config/tridactyl/tridactylrc
"
" "
" " Cleanup (requires Tridactyl 1.25.0+)
" " Persisted settings may briefly apply at startup before this file is sourced.
" "
" sanitise tridactylconfig
" set configversion 2.0
"
" " horizontal scrolling layout for reader
" colours --module=reader --url=https://raw.githubusercontent.com/tridactyl/tridactyl/refs/heads/master/contrib/themes/reader/newspaper.css newspaper
"
" colours quakelight
" set tabsort mru
"
"
" "
@ -76,7 +89,7 @@
" " bindurl ^https://duckduckgo.com F hint -Jbc [data-testid="result-title-a"]
" " Make `gi` on GitHub take you to the search box
" bindurl ^https://github.com gi hint -Vc .AppHeader-searchButton
" bindurl ^https://github.com gi hint -!Jc [aria-label*="Search"]
"
" " Allow Ctrl-a to select all in the commandline
" unbind --mode=ex <C-a>
@ -98,11 +111,29 @@
" bind gr reader
" bind gR reader --tab
"
" " Suspend / "discard" all tabs - handy for stretching out battery life
" command discardall jsb browser.tabs.query({}).then(ts => browser.tabs.discard(ts.map(t=>t.id)))
" " Use experimental :find mode
" bind / fillcmdline find -r
" bind ? fillcmdline find -r --reverse
" bind n findnext --search-from-view
" bind <C-g> findnext --search-from-view
" bind N findnext --search-from-view --reverse
" bind <C-G> findnext --search-from-view --reverse
" bind gN findselect
" " conflicts with quickmarks
" " bind gN composite findnext --search-from-view --reverse; findselect
" bind ,<Space> nohlsearch
"
" " Suspend / "discard" all tabs - handy for stretching out battery life. Now a default command, aliased here for my muscle memory
" command discardall tabdiscard --all
"
" " Julia docs' built in search is bad
" set searchurls.julia https://www.google.com/search?q=site:http://docs.julialang.org/en/v1%20
" " Search the current website with `open site [query]`
" set jsurls.site async q => `https://www.google.com/search?num=100&q=${encodeURIComponent("site:" + new URL((await tri.webext.activeTab()).url).hostname + " " + q)}`
"
" " Free up ctrl+[ for back in case H etc. has been stolen
" unbind --mode=normal <C-[>
"
" " get rid of annoying google 'ai overviews'
" set searchengine google
"
" "
" " Misc settings
@ -154,6 +185,9 @@
" " Quickmark for PRs by humans
" quickmark p https://github.com/tridactyl/tridactyl/pulls?q=is%3Apr+is%3Aopen+-label%3Adependencies+-author%3Abovine3dom+sort%3Aupdated-desc
" " Quick issue navigation
" bind --mode=normal ,i js tri.excmds.gobble("<CR>", 'js -d£ window.location.href = "https://github.com/tridactyl/tridactyl/issues/" + JS_ARGS[1]£')
"
" " Inject Google Translate
" " This (clearly) is remotely hosted code. Google will be sent the whole

29
AGENTS.md Normal file
View file

@ -0,0 +1,29 @@
# Agent guidelines from contributing to Tridactyl
This document defines how AI agents such as GitHub Copilot, Claude, OpenClaw and OpenCode must behave when interacting with this repository and contributing code. AI agents are not specifically encouraged to contribute to Tridactyl, but, equally, small and concise pull requests that cleanly fix real issues are welcome.
Your code will be reviewed by a time-pressed human. Pull requests that do not follow these guidelines will be rejected without further review. At the time of writing, there were approximately 80 open pull requests and the mean time from opening a pull request to getting it merged was about 90 days.
## Before you start coding
- Make sure you define success or failure for your patch and that it corresponds clearly to an issue.
- Make sure you can reproduce the issue locally.
- CRITICAL: do NOT work on a 'help wanted' issue for your first patch. These issues are the hardest and most complex and are difficult to reproduce.
## Patch contents
- All listed commit authors and co-authors must be human and their git email addresses must be monitored. Do not use `noreply` email addresses.
- CRITICAL: your patch must _ONLY_ make changes to relevant files and lines. Pull requests that change formatting on unrelated lines or add cruft to the repository will be rejected without review.
- CRITICAL: your first pull request _MUST_ be FEWER than 50 lines of code. Once a pull request has been merged, _SUBSEQUENT_ pull requests may be up to 100 lines of code. If it is not possible to fix the issue within these limits, choose a different issue.
- Tridactyl is a complex piece of software which interacts with Firefox, one of the most complex pieces of software on the planet. You should create failing end to end tests before writing any code and only submit a patch once these tests are pass. You should not edit the failing tests once you have started writing any code. _DO NOT_ submit new end-to-end tests as part of your patch: these are for your own use only.
## Making the pull request
- CRITICAL: your pull requests and commits _MUST_ be prefixed with the 🤖 emoji. Pull requests that seem to be created by agents but do not have this prefix will be REJECTED WITHOUT REVIEW.
- CRITICAL: you must make a HUMAN test that your patch fixes the original issue and you must document the steps they took to do so. If you cannot find a human to test your patch, you MUST document this in your pull request and explain what steps you took to ensure that the patch truly fixes the issue it claims to in an end-to-end fashion.

File diff suppressed because it is too large Load diff

13
browser-targets.json Normal file
View file

@ -0,0 +1,13 @@
{
"firefox": {
"minimumVersion": "94.0",
"manifestVersionPath": [
"applications",
"gecko",
"strict_min_version"
]
},
"chrome": {
"minimumVersion": "114"
}
}

12
ci/check-commit-emails.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
set -eu
authors=$(git log --format='%H%x09%an%x09%ae' "$BASE_SHA..$HEAD_SHA")
coauthors=$(git log --format='%H%x09%(trailers:key=Co-authored-by)' "$BASE_SHA..$HEAD_SHA")
bad_authors=$(printf '%s\n' "$authors" | grep -iE 'noreply' | grep -ivE 'dependabot(\[bot\])?' || true)
bad_coauthors=$(printf '%s\n' "$coauthors" | grep -iE 'noreply' || true)
if [ -n "$bad_authors$bad_coauthors" ]; then
printf '%s\n%s\n' "$bad_authors" "$bad_coauthors"
echo "'noreply' address found in patch. Please provide real contact details or ask a maintainer to adopt your commits."
exit 1
fi

View file

@ -12,3 +12,18 @@ if [ "$incompatible_sed" ]; then
fi
yarn run lint
"$(yarn bin)/eslint" --rulesdir custom-eslint-rules --ext .ts .
if [ "$(git rev-parse --abbrev-ref HEAD)" = "master" ]; then
if ! yarn prettier --check '**/*.ts' ; then
echo "Warning: the files above have prettier formatting issues. Run 'yarn run prettier -w [files]' to fix them."
fi
else
git fetch origin master 2>/dev/null || true
changed=$(git diff --name-only origin/master...HEAD -- '*.ts' 2>/dev/null || true)
if [ -n "$changed" ]; then
if ! yarn prettier --check $changed; then
echo "The files above have prettier formatting issues. Run 'yarn run prettier -w [files]' to fix them."
exit 1
fi
fi
fi

View file

@ -1,274 +0,0 @@
import * as ts from "typescript"
import * as fs from "fs"
import * as commandLineArgs from "command-line-args"
import * as AllTypes from "./types/AllTypes"
import * as AllMetadata from "./metadata/AllMetadata"
export function toSimpleType(typeNode) {
switch (typeNode.kind) {
case ts.SyntaxKind.VoidKeyword:
return new AllTypes.VoidType()
// IndexedAccessTypes are things like `fn<T keyof Class>(x: T, y: Class[T])`
// This doesn't seem to be easy to deal with so let's kludge it for now
case ts.SyntaxKind.IndexedAccessType:
// Unknown is just like any, but slightly stricter
case ts.SyntaxKind.UnknownKeyword:
case ts.SyntaxKind.AnyKeyword:
return new AllTypes.AnyType()
case ts.SyntaxKind.BooleanKeyword:
return new AllTypes.BooleanType()
case ts.SyntaxKind.NumberKeyword:
return new AllTypes.NumberType()
case ts.SyntaxKind.ObjectKeyword:
return new AllTypes.ObjectType()
case ts.SyntaxKind.StringKeyword:
return new AllTypes.StringType()
case ts.SyntaxKind.Parameter:
let n = toSimpleType(typeNode.type)
n.name = typeNode.name.original.escapedText
n.isDotDotDot = !!typeNode.dotDotDotToken
n.isQuestion = !!typeNode.questionToken
return n
case ts.SyntaxKind.TypeReference:
if (!typeNode.typeArguments) {
// If there are no typeArguments, this is not a parametric type and we can return the type directly
try {
return toSimpleType(
typeNode.typeName.symbol.declarations[0].type,
)
} catch (e) {
// Fall back to what you'd do with typeArguments
}
}
let args = typeNode.typeArguments
? typeNode.typeArguments.map(t =>
toSimpleType(typeNode.typeArguments[0]),
)
: []
return new AllTypes.TypeReferenceType(
typeNode.typeName.escapedText,
args,
)
case ts.SyntaxKind.FunctionType:
// generics = (typeNode.typeParameters || []).map(p => new AllTypes.SimpleType(p))
return new AllTypes.FunctionType(
typeNode.parameters.map(p => toSimpleType(p)),
toSimpleType(typeNode.type),
)
case ts.SyntaxKind.TypeLiteral:
let members = typeNode.members
.map(member => {
if (member.kind == ts.SyntaxKind.IndexSignature) {
// Something like this: { [str: string]: string[] }
return ["", toSimpleType(member.type)]
}
// Very fun feature: when you have an object literal with >20 members, typescript will decide to replace some of them with a "... X more ..." node that obviously doesn't have a corresponding symbol, hence this check and the filter after the map
if (member.name.symbol)
return [
member.name.symbol.escapedName,
toSimpleType(member.type),
]
})
.filter(m => m)
return new AllTypes.ObjectType(new Map(members))
case ts.SyntaxKind.ArrayType:
return new AllTypes.ArrayType(toSimpleType(typeNode.elementType))
case ts.SyntaxKind.TupleType:
return new AllTypes.TupleType(
typeNode.elementTypes.map(t => toSimpleType(t)),
)
case ts.SyntaxKind.UnionType:
return new AllTypes.UnionType(
typeNode.types.map(t => toSimpleType(t)),
)
break
case ts.SyntaxKind.LiteralType:
return new AllTypes.LiteralTypeType(typeNode.literal.text)
break
default:
console.log(typeNode)
throw new Error(`Unhandled kind (${typeNode.kind}) for ${typeNode}`)
}
}
/** True if node is visible outside its file, false otherwise */
function isNodeExported(node: ts.Node): boolean {
return (
(ts.getCombinedModifierFlags(<ts.Declaration>node) &
ts.ModifierFlags.Export) !==
0 ||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
)
}
/** True if node is marked as @hidden in its documentation */
function isNodeHidden(sourceFile, node): boolean {
return (
sourceFile &&
node.jsDoc &&
!!node.jsDoc.find(
doc =>
sourceFile.text.slice(doc.pos, doc.end).search("@hidden") != -1,
)
)
}
function visit(
checker: any,
sourceFile: any,
file: AllMetadata.FileMetadata,
node: any,
) {
let symbol = checker.getSymbolAtLocation(node.name)
if (symbol && isNodeExported(node)) {
let nodeName = symbol.escapedName
switch (node.kind) {
case ts.SyntaxKind.FunctionDeclaration:
// Grab the doc, default to empty string
let doc =
ts.displayPartsToString(symbol.getDocumentationComment()) ||
""
// Grab the type
let ttype = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!,
)
// If the function has a type, try to convert it, if it doesn't, default to any
let t = ttype
? toSimpleType(checker.typeToTypeNode(ttype))
: new AllTypes.AnyType()
file.setFunction(
nodeName,
new AllMetadata.SymbolMetadata(
doc,
t,
isNodeHidden(sourceFile, node),
),
)
return
case ts.SyntaxKind.ClassDeclaration:
let clazz = file.getClass(nodeName)
if (!clazz) {
clazz = new AllMetadata.ClassMetadata()
file.setClass(nodeName, clazz)
}
symbol.members.forEach((sym, name, map) => {
// Can't get doc/type from these special functions
// Or at least, it requires work that might not be needed for now
if (["__constructor", "get", "set"].includes(name)) return
// Grab the doc, default to empty string
let doc =
ts.displayPartsToString(
sym.getDocumentationComment(),
) || ""
// Grab the type
let ttype = checker.getTypeOfSymbolAtLocation(
sym,
sym.valueDeclaration!,
)
// If the function has a type, try to convert it, if it doesn't, default to any
let t = ttype
? toSimpleType(checker.typeToTypeNode(ttype))
: new AllTypes.AnyType()
clazz.setMember(
name,
new AllMetadata.SymbolMetadata(
doc,
t,
isNodeHidden(sourceFile, node),
),
)
})
return
// Other declaration syntaxkinds:
// case ts.SyntaxKind.VariableDeclaration:
// case ts.SyntaxKind.VariableDeclarationList:
// case ts.SyntaxKind.PropertyDeclaration:
// case ts.SyntaxKind.MethodDeclaration:
// case ts.SyntaxKind.EndOfDeclarationMarker:
// case ts.SyntaxKind.MergeDeclarationMarker:
// case ts.SyntaxKind.MissingDeclaration:
// case ts.SyntaxKind.ClassExpression:
// case ts.SyntaxKind.InterfaceDeclaration:
// case ts.SyntaxKind.TypeAliasDeclaration:
// case ts.SyntaxKind.EnumDeclaration:
// case ts.SyntaxKind.ModuleDeclaration:
// case ts.SyntaxKind.ImportEqualsDeclaration:
// case ts.SyntaxKind.ImportDeclaration:
// case ts.SyntaxKind.NamespaceExportDeclaration:
// case ts.SyntaxKind.ExportDeclaration:
// case ts.SyntaxKind.Constructor:
}
}
ts.forEachChild(node, node => visit(checker, sourceFile, file, node))
}
function generateMetadata(
out: string,
themedir: string,
fileNames: string[],
): void {
/* Parse Tridactyl */
let program = ts.createProgram(fileNames, {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
})
let metadata = new AllMetadata.ProgramMetadata()
for (const sourceFile of program.getSourceFiles()) {
let name = (fileNames as any).find(name =>
sourceFile.fileName.match(name),
)
if (name) {
let file = metadata.getFile(name)
if (!file) {
file = new AllMetadata.FileMetadata()
metadata.setFile(name, file)
}
visit(program.getTypeChecker(), sourceFile, file, sourceFile)
}
}
// We need to specify Type itself because it won't exist in AllTypes.js since it's an interface
let imports =
`import { Type } from "../compiler/types/AllTypes"\n` +
`import {${Object.keys(AllTypes).join(
", ",
)}} from "../compiler/types/AllTypes"\n` +
`import {${Object.keys(AllMetadata).join(
", ",
)}} from "../compiler/metadata/AllMetadata"\n`
let metadataString =
imports + `\nexport let everything = ${metadata.toConstructor()}\n`
if (themedir) {
metadataString += `\nexport let staticThemes = ${JSON.stringify(
fs.readdirSync(themedir),
)}\n`
}
// print out the doc
fs.writeFileSync(out, metadataString)
return
}
let opts = commandLineArgs([
{ name: "out", type: String },
{ name: "themeDir", type: String },
{ name: "src", type: String, multiple: true, defaultOption: true },
])
if (!opts.out || opts.src.length < 1)
throw new Error(
"Argument syntax: --out outfile [--src] file1.ts [file2.ts ...]",
)
generateMetadata(opts.out, opts.themeDir, opts.src)

View file

@ -1,4 +0,0 @@
export { SymbolMetadata } from "./SymbolMetadata"
export { ClassMetadata } from "./ClassMetadata"
export { FileMetadata } from "./FileMetadata"
export { ProgramMetadata } from "./ProgramMetadata"

View file

@ -1,33 +0,0 @@
import { SymbolMetadata } from "./SymbolMetadata"
export class ClassMetadata {
constructor(
public members: Map<string, SymbolMetadata> = new Map<
string,
SymbolMetadata
>(),
) {}
public setMember(name: string, s: SymbolMetadata) {
this.members.set(name, s)
}
public getMember(name: string) {
return this.members.get(name)
}
public getMembers() {
return this.members.keys()
}
public toConstructor() {
return (
`new ClassMetadata(new Map<string, SymbolMetadata>([` +
Array.from(this.members.entries())
.map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -1,57 +0,0 @@
import { ClassMetadata } from "./ClassMetadata"
import { SymbolMetadata } from "./SymbolMetadata"
export class FileMetadata {
constructor(
public classes: Map<string, ClassMetadata> = new Map<
string,
ClassMetadata
>(),
public functions: Map<string, SymbolMetadata> = new Map<
string,
SymbolMetadata
>(),
) {}
public setClass(name: string, c: ClassMetadata) {
this.classes.set(name, c)
}
public getClass(name: string) {
return this.classes.get(name)
}
public getClasses() {
return Array.from(this.classes.keys())
}
public setFunction(name: string, f: SymbolMetadata) {
this.functions.set(name, f)
}
public getFunction(name: string) {
return this.functions.get(name)
}
public getFunctions() {
return Array.from(this.functions.entries())
}
public getFunctionNames() {
return Array.from(this.functions.keys())
}
public toConstructor() {
return (
`new FileMetadata(new Map<string, ClassMetadata>([` +
Array.from(this.classes.entries())
.map(([n, c]) => `[${JSON.stringify(n)}, ${c.toConstructor()}]`)
.join(",\n") +
`]), new Map<string, SymbolMetadata>([` +
Array.from(this.functions.entries())
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -1,28 +0,0 @@
import { FileMetadata } from "./FileMetadata"
export class ProgramMetadata {
constructor(
public files: Map<string, FileMetadata> = new Map<
string,
FileMetadata
>(),
) {}
public setFile(name: string, file: FileMetadata) {
this.files.set(name, file)
}
public getFile(name: string) {
return this.files.get(name)
}
public toConstructor() {
return (
`new ProgramMetadata(new Map<string, FileMetadata>([` +
Array.from(this.files.entries())
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
.join(",\n") +
`]))`
)
}
}

View file

@ -1,11 +0,0 @@
import { Type } from "../types/AllTypes"
export class SymbolMetadata {
constructor(public doc: string, public type: Type, public hidden = false) {}
public toConstructor() {
return `new SymbolMetadata(${JSON.stringify(
this.doc,
)}, ${this.type.toConstructor()}, ${this.hidden})`
}
}

View file

@ -1,13 +0,0 @@
export { Type } from "./Type"
export { AnyType } from "./AnyType"
export { BooleanType } from "./BooleanType"
export { FunctionType } from "./FunctionType"
export { NumberType } from "./NumberType"
export { ObjectType } from "./ObjectType"
export { StringType } from "./StringType"
export { TypeReferenceType } from "./TypeReferenceType"
export { VoidType } from "./VoidType"
export { ArrayType } from "./ArrayType"
export { LiteralTypeType } from "./LiteralTypeType"
export { TupleType } from "./TupleType"
export { UnionType } from "./UnionType"

View file

@ -1,19 +0,0 @@
import { Type } from "./Type"
export class AnyType implements Type {
public kind = "any"
constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new AnyType(${!this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
return argument
}
}

View file

@ -1,29 +0,0 @@
import { Type } from "./Type"
export class ArrayType implements Type {
public kind = "array"
constructor(public elemType: Type, public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new ArrayType(${this.elemType.toConstructor()}, ${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return `${this.elemType.toString()}[]`
}
public convert(argument) {
if (!Array.isArray(argument)) {
try {
argument = JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert ${argument} to array:`)
}
if (!Array.isArray(argument)) {
throw new Error(`Can't convert ${argument} to array:`)
}
}
return argument.map(v => this.elemType.convert(v))
}
}

View file

@ -1,24 +0,0 @@
import { Type } from "./Type"
export class BooleanType implements Type {
public kind = "boolean"
constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new BooleanType(${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
if (argument === "true") {
return true
} else if (argument === "false") {
return false
}
throw new Error("Can't convert ${argument} to boolean")
}
}

View file

@ -1,28 +0,0 @@
import { Type } from "./Type"
export class FunctionType implements Type {
public kind = "function"
constructor(public args: Type[], public ret: Type, public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return (
`new FunctionType([` +
// Convert every argument type to its string constructor representation
this.args.map(cur => cur.toConstructor()) +
`], ${this.ret.toConstructor()}, ${this.isDotDotDot}, ${this.isQuestion})`
)
}
public toString() {
return `(${this.args.map(a => a.toString()).join(", ")}) => ${this.ret.toString()}`
}
public convert(argument) {
// Possible strategies:
// - eval()
// - window[argument]
// - tri.excmds[argument]
throw new Error(`Conversion to function not implemented: ${argument}`)
}
}

View file

@ -1,26 +0,0 @@
import { Type } from "./Type"
export class LiteralTypeType implements Type {
public kind = "LiteralType"
constructor(public value: string, public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new LiteralTypeType(${JSON.stringify(this.value)}, ${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return JSON.stringify(this.value)
}
public convert(argument) {
if (argument === this.value) {
return argument
}
throw new Error(
`Argument does not match expected value (${
this.value
}): ${argument}`,
)
}
}

View file

@ -1,23 +0,0 @@
import { Type } from "./Type"
export class NumberType implements Type {
public kind = "number"
public constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new NumberType(${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
const n = parseFloat(argument)
if (!Number.isNaN(n)) {
return n
}
throw new Error(`Can't convert to number: ${argument}`)
}
}

View file

@ -1,43 +0,0 @@
import { Type } from "./Type"
export class ObjectType implements Type {
public kind = "object"
// Note: a map that has an empty key ("") uses the corresponding type as default type
constructor(public members: Map<string, Type> = new Map<string, Type>(), public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new ObjectType(new Map<string, Type>([` +
Array.from(this.members.entries()).map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
.join(", ") +
`]), ${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convertMember(memberName: string[], memberValue: string) {
let type = this.members.get(memberName[0])
if (!type) {
// No type, try to get the default type
type = this.members.get("")
if (!type) {
// No info for this member and no default type, anything goes
return memberValue
}
}
if (type.kind === "object") {
return (type as ObjectType).convertMember(memberName.slice(1), memberValue)
}
return type.convert(memberValue)
}
public convert(argument) {
try {
return JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert to object: ${argument}`)
}
}
}

View file

@ -1,22 +0,0 @@
import { Type } from "./Type"
export class StringType implements Type {
public kind = "string"
constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new StringType(${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
if (typeof argument === "string") {
return argument
}
throw new Error(`Can't convert to string: ${argument}`)
}
}

View file

@ -1,39 +0,0 @@
import { Type } from "./Type"
export class TupleType implements Type {
public kind = "tuple"
constructor(public elemTypes: Type[], public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return (
`new TupleType([` +
// Convert every element type to its constructor representation
this.elemTypes.map(cur => cur.toConstructor()).join(",\n") +
`], ${this.isDotDotDot}, ${this.isQuestion})`
)
}
public toString() {
return `[${this.elemTypes.map(e => e.toString()).join(", ")}]`
}
public convert(argument) {
if (!Array.isArray(argument)) {
try {
argument = JSON.parse(argument)
} catch (e) {
throw new Error(`Can't convert to tuple: ${argument}`)
}
if (!Array.isArray(argument)) {
throw new Error(`Can't convert to tuple: ${argument}`)
}
}
if (argument.length !== this.elemTypes.length) {
throw new Error(
`Error converting tuple: number of elements and type mismatch ${argument}`,
)
}
return argument.map((v, i) => this.elemTypes[i].convert(v))
}
}

View file

@ -1,12 +0,0 @@
export interface Type {
// Only available on argument types
name?: string
isDotDotDot?: boolean
isQuestion?: boolean
// available everywhere
kind: string
toConstructor(): string
toString(): string
convert(argument: string): any
}

View file

@ -1,22 +0,0 @@
import { Type } from "./Type"
export class TypeReferenceType implements Type {
public constructor(public kind: string, public args: Type[], public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return (
`new TypeReferenceType(${JSON.stringify(this.kind)}, [` +
// Turn every type argument into its constructor representation
this.args.map(cur => cur.toConstructor()).join(",\n") +
`], ${this.isDotDotDot}, ${this.isQuestion})`
)
}
public toString() {
return `${this.kind}<${this.args.map(a => a.toString()).join(", ")}>`
}
public convert(argument) {
throw new Error("Conversion of simple type references not implemented.")
}
}

View file

@ -1,29 +0,0 @@
import { Type } from "./Type"
export class UnionType implements Type {
public kind = "union"
constructor(public types: Type[], public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return (
`new UnionType([` +
// Convert every type to its string constructor representation
this.types.map(cur => cur.toConstructor()).join(",\n") +
`], ${this.isDotDotDot}, ${this.isQuestion})`
)
}
public toString() {
return this.types.map(t => t.toString()).join(" | ")
}
public convert(argument) {
for (const t of this.types) {
try {
return t.convert(argument)
} catch (e) {}
}
throw new Error(`Can't convert "${argument}" to any of: ${this.types}`)
}
}

View file

@ -1,19 +0,0 @@
import { Type } from "./Type"
export class VoidType implements Type {
public kind = "void"
constructor(public isDotDotDot = false, public isQuestion = false) {}
public toConstructor() {
return `new VoidType(${this.isDotDotDot}, ${this.isQuestion})`
}
public toString() {
return this.kind
}
public convert(argument) {
return null
}
}

3
contrib/readme.md Normal file
View file

@ -0,0 +1,3 @@
# Tridactyl `contrib`
Not sure what we'll put in this directory, but it won't be as polished as the rest of Tridactyl.

View file

@ -0,0 +1,112 @@
/*
* Sidescrolling newspaper-style layout for :reader
*
* Author: bovine3dom
*
* Usage: `:colourscheme --module=reader --url=[path to raw version of this file] newspaper`
*
*/
:root {
--serif-font: EconomistSerif, Plantin, Garamond, serif;
}
html:not(.TridactylCommandline) {
height: 100vh;
width: 100vw;
overflow: hidden;
font-family: var(--serif-font);
}
:not(.TridactylCommandline) {
body {
display: block;
height: 100%;
width: 100%;
padding: 2rem;
column-width: 24rem;
column-gap: 3rem;
column-fill: auto;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: auto;
}
body > main {
display: contents;
}
body > header,
body > footer,
figure, section, article, .notice,
h1, h2, h3, h4, h5, h6,
img, video, table {
break-inside: avoid;
}
h1, h2, h3, h4, h5, h6 { break-after: avoid-column; }
h1 + *, h2 + *, h3 + *, h4 + *, h5 + *, h6 + * { break-before: avoid-column; }
img, video, table, pre, figure {
display: block;
scrollbar-width: thin;
}
img, video, table {
max-width: 100%;
}
table, pre, figure, img {
width: 100%;
max-height: calc(100vh - 6rem);
overflow-y: auto;
}
body > header {
display: block;
width: 100%;
border-bottom: 2px solid var(--accent);
margin-bottom: 2rem;
}
body > footer {
display: block;
margin-top: 4rem;
border-top: 2px solid var(--accent);
background-color: var(--accent-bg);
}
body > header h1,
body > header p {
margin: 0.5rem auto;
}
body > header h1 { max-width: 100%; }
h1 { font-size: 2rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.7rem; }
h2, h3 { margin-top: 0.5rem; }
@media only screen and (max-width: 600px) {
html, body {
height: auto;
display: block;
overflow-x: hidden;
overflow-y: auto;
columns: 1 auto;
padding: 0;
}
body > main {
display: block;
padding: 1rem;
}
body > header {
margin-bottom: 0;
}
}
}

View file

@ -22,7 +22,7 @@ Tridactyl is very lucky to have a wide base of contributors, 30 at the time of w
- You could work on some feature that you really want to see in Tridactyl that we haven't even thought of yet.
- Our build process is a bit convoluted, but [excmds.ts][excmds] is probably where you want to start. Most of the business happens there.
- We use TypeDoc to produce the `:help` page. Look at the other functions in [excmds.ts][excmds] to get an idea of how to use it; if your function is not supposed to called from the command line, then please add `/** @hidden */` above it to prevent it being shown on the help page.
- Our pre-commit hook runs prettier to format your code. Please don't circumvent it.
- Email addresses mentioned in commits must be monitored. Do not use `noreply` addresses. If you have a good reason why you cannot provide a monitored email address, you can ask a maintainer to adopt your commits under their email address.
If you are making a substantial or potentially controversial change, your first port of call should be to stop by and chat to us on [Matrix][matrix] or file an issue to discuss what you would like to change. We really don't want you to waste time on a pull request (GitHub jargon for a contribution) that has no chance of being merged; that said, we are probably happy to gate even the most controversial changes behind an option.

View file

@ -0,0 +1,90 @@
const bcd = require("@mdn/browser-compat-data")
const api = bcd.webextensions.api
function propertyNameOrValue(n) {
return n.property.type == "Literal" ? n.property.value : n.property.name
}
function isVersionNewer(version, minimumVersion) {
const versionParts = String(version).split(".").map(Number)
const minimumParts = String(minimumVersion).split(".").map(Number)
if (versionParts.some(Number.isNaN) || minimumParts.some(Number.isNaN))
return false
const length = Math.max(versionParts.length, minimumParts.length)
for (let i = 0; i < length; i++) {
const difference = (versionParts[i] || 0) - (minimumParts[i] || 0)
if (difference !== 0) return difference > 0
}
return false
}
function detectBrowserUsage(context, node, browser, minimumVersion) {
let localApi = api
const fullName = []
while (
node.type == "MemberExpression" &&
propertyNameOrValue(node) in localApi
) {
const n = node
node = node.parent
const name = propertyNameOrValue(n)
fullName.push(name)
localApi = localApi[name]
if (!localApi.__compat) {
continue
}
const support = localApi.__compat.support
if (support[browser].version_added === false) {
context.report({
node: n,
messageId: "unsupportedApis",
data: {
name: browser,
api: fullName.join("."),
},
})
} else {
const version = support[browser].version_added
if (
minimumVersion !== undefined &&
isVersionNewer(version, minimumVersion)
) {
context.report({
node: n,
messageId: "apiTooRecent",
data: {
api: fullName.join("."),
name: browser,
version: minimumVersion,
},
})
}
}
}
}
module.exports = browser => ({
meta: {
schema: [
{
type: "object",
properties: { minimumVersion: { type: "string" } },
additionalProperties: false,
},
],
messages: {
unsupportedApis: "{{ api }} unsupported on '{{ name }}'",
apiTooRecent:
"{{ api }} is not supported on {{ name }} {{ version }}",
},
},
create(context) {
const { minimumVersion } = context.options[0] || {}
const detect = node =>
detectBrowserUsage(context, node, browser, minimumVersion)
return {
'MemberExpression[object.name="browser"]': detect,
'MemberExpression[object.name="browserBg"]': detect,
}
},
})

View file

@ -0,0 +1,24 @@
# adding a new target
make a file here like
```js
// unsupported-apis-mosaic.js
module.exports = require("./lib/unsupported-apis")("mosaic")
```
add a line to `../browser-targets.json`
```json
"mosaic": { "minimumVersion": "1.0.0" }
```
add a few lines to `../.eslintrc.js`
```js
"unsupported-apis-mosaic": [
"warn",
{ "minimumVersion": browserTargets.mosaic.minimumVersion }
],
```

View file

@ -0,0 +1 @@
module.exports = require("./lib/unsupported-apis")("chrome")

View file

@ -0,0 +1 @@
module.exports = require("./lib/unsupported-apis")("firefox")

View file

@ -1,63 +0,0 @@
const bcd = require('@mdn/browser-compat-data');
const api = bcd.webextensions.api;
const supported_browsers = ["firefox", "chrome"];
const minimalSupportedFirefoxVersion = 114;
function propertyNameOrValue(n) {
return (n.property.type == "Literal" ? n.property.value : n.property.name)
}
function detectBrowserUsage(context, node) {
let localApi = api;
let fullName = [];
while (node.type == "MemberExpression" && propertyNameOrValue(node) in localApi) {
const n = node;
node = node.parent;
let name = propertyNameOrValue(n);
fullName.push(name);
localApi = localApi[name];
if (!localApi.__compat) {
continue;
}
let support = localApi.__compat.support;
for (let browser of supported_browsers) {
if (support[browser].version_added === false) {
context.report({
node: n,
messageId: "unsupportedApis",
data: {
name: browser,
api: fullName.join("."),
}
});
} else {
const version = Number(support[browser].version_added);
if (!isNaN(version) && version > minimalSupportedFirefoxVersion) {
context.report({
node: n,
messageId: "apiTooRecent",
data: {
api: fullName.join("."),
version: minimalSupportedFirefoxVersion
}
});
}
}
}
}
}
module.exports = {
meta: {
messages: {
unsupportedApis: "{{ api }} unsupported on '{{ name }}'",
apiTooRecent: "{{ api }} is not supported on firefox {{ version }}",
}
},
create(context) {
return {
'MemberExpression[object.name="browser"]': (n) => detectBrowserUsage(context, n),
'MemberExpression[object.name="browserBg"]': (n) => detectBrowserUsage(context, n),
};
}
};

View file

@ -1,6 +1,6 @@
**Control your browser with your keyboard _only_.**
Replace Firefox's control mechanism with one modelled on VIM. This is a "Firefox Quantum" replacement for VimFX, Vimperator and Pentadactyl. Most common tasks you want your browser to perform are bound to a single key press:
Replace Firefox's control mechanism with one modelled on VIM. We were heavily inspired by VimFX, Vimperator and Pentadactyl, but more modern equivalents are Vimium or qutebrowser. Most common tasks you want your browser to perform are bound to a single key press:
- You want to open a new tab? Hit `t`.
- You want to follow that link? Hit `f` and type the displayed label. (Note: hint characters should be typed in lowercase.)
@ -52,14 +52,16 @@ Since Tridactyl aims to provide all the features Vimperator and Pentadactyl had,
- Access recently closed tabs:
- If you've accidentally closed a tab or window, Tridactyl will let you open it again with the `:undo` command which is bound to `u` by default.
- Access browser tabs:
- Tridactyl provides a quick tab-switching menu/command with the `:buffer` command (bound to `b`). This permission is also required to close, move, and pin tabs, amongst other things.
- Tridactyl provides a quick tab-switching menu/command with the `:tab` command (bound to `b`). This permission is also required to close, move, and pin tabs, amongst other things.
- Access browser activity during navigation:
- This is needed for Tridactyl to be able to go back to normal mode every time you open a new page. In the future we may use it for autocommands.
- This is needed for Tridactyl to be able to go back to normal mode every time you open a new page. It is also used for autocommands.
- Read the text of all open tabs:
- This allows us to use Firefox's built-in find-in-page API, for, for example, allowing you to bind find-next and find-previous to `n` and `N`.
- Monitor extension usage and manage themes:
- Tridactyl needs this to integrate with and avoid conflicts with other extensions. For example, Tridactyl's contextual identity features use this to cooperate with the Multi-Account Containers extension.
- Hide tabs:
- Hide and show browser tabs:
- Tridactyl needs this for tab group commands, which allow associating names with different groups of tabs and showing the tabs from only of those groups at a time.
- Control browser proxy settings:
- This allows you to set proxies for accessing different websites with :autocontain.
[betas]: https://tridactyl.cmcaine.co.uk/betas/?sort=time&order=desc

44
doc/glossary.jsonl Normal file
View file

@ -0,0 +1,44 @@
{"word": "mozilla", "definition": "The foundation that controls Firefox and Thunderbird."}
{"word": "bovine3dom", "definition": "The esteemed maintainer of Tridactyl and author of this glossary."}
{"word": "vimperator", "definition": "A now defunct Firefox extension that was the main inspiration for Tridactyl."}
{"word": "commandline", "definition": "A text-based interface to Tridactyl for running commands interactively and selecting from completion options, also called ex-mode. By default, bound to `:`"}
{"word": "completion", "definition": "An object that can be selected from the commandline to complete a partially typed command, such as a URL for `:open` or a tab for `:tab`"}
{"word": "keybind", "definition": "A relationship between a key press or series of key presses and a command"}
{"word": "keysequence", "definition": "A series of key presses or key combinations that are descibed using a special language where, e.g. `<C-j>` represents `ctrl + j`"}
{"word": "keydown", "definition": "An event triggered when a key is first pressed, represented in keysequeences as `<D-[character]>`"}
{"word": "keyup", "definition": "The event triggered when a key is released, represented in keysequences as `<U-[character]>`"}
{"word": "keypress", "definition": "An event triggered when a key is pressed and periodically while it is held down, represented in keysequences as `[character]`"}
{"word": "pentadactyl", "definition": "A now defunct fork of Vimperator and one of the key inspirations for Tridactyl."}
{"word": "treestyletabs", "definition": "A popular Firefox extension that adds a vertical tree of tabs to the user interface"}
{"word": "tridactyl", "definition": "The best Firefox extension on the planet and the one you are using right now. Accept no imitations!"}
{"word": "userchrome", "definition": "A semi-secret file that Firefox reads, if the preferences are set correctly, and allows you to edit the GUI. Tridactyl has a helper function for this called `:guiset`"}
{"word": "tridactylrc", "definition": "A 'run commands' file that simply runs a series of commands at startup. Many users use these files to 'store' their settings, but it is important to remember that it really is just a bunch of commands to run at startup. See `:source`, `:mkt` and `:native`"}
{"word": "xclip", "definition": "A command line utility that allows you to manipulate the clipboard under Linux and X11. See `:help yankto` and `:help putfrom`"}
{"word": "xsel", "definition": "A command line utility that allows you to manipulate the clipboard under Linux and X11. See `:help yankto` and `:help putfrom`"}
{"word": "sandboxed", "definition": "A security feature that prevents processes such as Firefox from accessing the file system, which breaks most of `:native` without some extra fiddling and whitelisting. 'snap' and 'flatpak' are common forms of sandboxing."}
{"word": "github", "definition": "A popular code hosting site (aka 'forge') that allows you to store, share and discuss code with other users. At the time of writing, it's where Tridactyl was mostly hosted, although it's important to remember that `git` is decentralised and so really Tridactyl is hosted by developer who has ever cloned it"}
{"word": "mappings", "definition": "Another word for keybinds"}
{"word": "rebind", "definition": "To change the keybind a command is associated with"}
{"word": "gitter", "definition": "A now mostly dead chat service that has been replaced by Matrix, but still works"}
{"word": "deprecated", "definition": "A command or setting that will eventually be removed from Tridactyl, but works for now"}
{"word": "emacs", "definition": "An operating system that only lacks a decent text editor. Inexplicably popular despite the fact that `nano` and `notepad.exe` are superior and pre-installed on most machines"}
{"word": "popup", "definition": "A small window that appears in the middle of the screen, usually without consent to display something that the website wishes to annoy you with. Thankfully, web browsers block them universally, and so now they appear within webpages instead in the form of newsletter signups and 'cookie consent' dialogs, which is vastly better than having easily detectable new windows created because ... there's got to be a reason, right? Anyway, don't use the internet without an adblocker like uBlock Origin installed, kids"}
{"word": "javascript", "definition": "Brendan Eich's long-running joke played on humanity. Tragically, it is also the language in which we are forced to write Tridactyl."}
{"word": "firefox", "definition": "The web browser you're almost certainly using right now"}
{"word": "vim", "definition": "The best text editor in the universe, second only to 'ex'"}
{"word": "libera", "definition": "An IRC network that became popular after Freenode imploded. Come talk with us on #tridactyl like it's 2002"}
{"word": "wiki", "definition": "Tridactyl's largely unmoderated and well hidden wiki, stored on GitHub"}
{"word": "layout", "definition": "Which button inserts which character on a keyboard. You can change this in software."}
{"word": "rc", "definition": "Shorthand for 'tridactylrc'"}
{"word": "sidebar", "definition": "Part of the Firefox UI that appears on the left and extensions can put things in. We mostly use it for `:escapehatch` to get focus back onto the page, but `:sidebaropen` exists and has some enthusiastic users"}
{"word": "addon", "definition": "A Firefox extension such as Tridactyl"}
{"word": "matrix", "definition": "A chat network"}
{"word": "pipe", "definition": "A way of sending data between commands, usually represented by `|`"}
{"word": "messenger", "definition": "A small program that runs in userland and makes `:native` work for various tasks such as opening URLs Mozilla thinks are dangerous, starting system utilities like `xclip` or restarting the browser"}
{"word": "ex", "definition": "The original line-based text editor upon which Vim was initially based. The commandline is called 'ex mode' in honour of this, commands are called 'ex commands' and we sometimes call series of ex commands 'ex scripts'"}
{"word": "marks", "definition": "A way of remembering a position or URL"}
{"word": "issue", "definition": "A bug report, feature request or question on GitHub. We often talk about 'filing' issues to mean creating them"}
{"word": "recently", "definition": "Anything within the last decade"}
{"word": "control", "definition": "The key marked 'ctrl' on your keyboard. Unlike many other programs, we really do mean the key marked 'ctrl', and not the splat key on MacOS, which we call 'meta'"}
{"word": "jumplist", "definition": "A kind of history of bits of pages you have been on, accessible with `<C-o>` and `<C-i>`, and `g;` for text fields you have edited"}
{"word": "sponsor", "definition": "Someone who gives money to bovine3dom so he can keep working on Tridactyl and spend his time writing definitions for the glossary like he is doing right now"}

View file

@ -0,0 +1,55 @@
# Tridactyl news - Winter 2024
Hello,
Welcome to the tenth Tridactyl newsletter! It looks like I've fallen into a routine of doing them biennially; I'll try to make them at least once a year because we're adding stuff to Tridactyl at such a rate that it becomes a little difficult to summarise it all. On top of this, I wrote this newsletter in around December 2024 but somehow managed to forget to release it, although it was on a public git branch. But everything in it is still relevant, so I am publishing it now in December 2025. And I will aim to write an actually new newsletter in January/February. Thanks as ever for your patience :)
What follows is a brief exploration of the changes in Tridactyl since the last newsletter came out in February 2023.
## Highlighted new features
I'll go through a few of my favourite new features, ones that I myself am using daily. The first is that we added a new `:reader` mode, left unbound by default because while we take every precaution, running it on untrusted websites opens up a larger attack surface than using the built-in Firefox reader (which remains accessible on the `gr` bind and with the `:reader --old` command). You can of course bind to the new mode with `:bind gr reader`, which is what I personally do. This lets you use all of the Tridactyl commands and interface on a minimal "reader" style interface and seems in general to work on slightly more websites than the built-in mode. I use it on websites that go slightly overboard with the cookie warnings, modal newsletter pop-ups and banner adverts ... that is to say, most of them :)
A totally different but equally transformative feature we've added is the ability to sort tabs everywhere in Tridactyl in a "most recently used" fashion. By running `:set tabsort mru`, not only will `b`/`:tab` completions be sorted by recency, but within Tridactyl internally all the commands will use recency indices too, meaning that for example `[n]gt` will take you to the nth-most-recently-used tab rather than the tab with index n. I found it took about a week to get used to but now I can't imagine using Firefox and Tridactyl in any other way. It makes doing housekeeping on tabs easy too, because one can just press shift-tab to get to the end of the `:tab` list in recency to find the ones that haven't been used in a while, and then close them one by one with shift-delete.
For something a little more niche, Tridactyl's support for non-English keyboard layouts has got a little better with the addition of `:set keyboardlayoutforce true`. This forces Tridactyl to see your keyboard as if it was a different layout - by default, US, but configurable via `:set keyboardlayoutbase`. This means that for example, pressing the home row key for the right hand will always be interpreted as j, which is bound by default to scrolling down. If you're frequently switching between keyboard layouts so you can easily type in different languages (for example, colemak and bépo), your Tridactyl binds will stay in the same physical location.
A cool command that I've been using a lot personally and will be of interest to anyone who uses a mobile device is the new `yq` bind, which temporarily displays the current URL as a QR code. There's also a `q` bind in visual mode and a `:text2qr` command for general programmatic use.
A small quality of life improvement is that `:open` and related commands now have completions for searchurls and previous searches.
A few improvements have come to hints: there's now `:set hintstyles.{bg, fg, outline}` for changing the style of hints without changing your theme, `;c` and `;:` for context menu / mouse hover binds added for `:native` and `xdotool` users. There are also new flags: `-x` for excluding CSS selectors and `-C` for including extra CSS selectors alongside the defaults.
With the command line open, `<C-o>t` opens a new tab in the background for the selected completion which I find especially useful with `:back` completions. Don't forget that all binds for the command line can be viewed with `:viewconfig exmaps` and in `:bind --mode=ex ` completions - I think most people are unaware of them, which is a shame because `<S-Delete>` to close `:tab ` completions is one of my favourite Tridactyl features.
A pretty odd feature that I am not totally unconvinced of the utility of is that we now have experimental support for opening arbitrary pages in the sidebar. See `:help sidebartoggle` and `:help sidebaropen` for more details; it works somewhat well with `:hint -W sidebaropen` but again I'm still not sure what it's useful for. Feel free to tell us if you do find it useful.
An extremely niche feature is the new `:jsua` command which preserves "user action" intent when using browser binds, needed for triggering certain Web Extension APIs such as `browser.sidebarAction.open()`. I suspect there are about three people in the world who care about this :). We've also added `tri.bg` and `tri.tabs` for easy communication between tab and background contexts in `:jsb` and `:js`, see their help pages for more information.
Something that I suspect will interest people who don't read this newsletter more than people who do is that we've started the groundwork for multi-browser support in our linting scripts. We haven't considered how to go about Manifest V3 support at all, so support for Chromium would still be a long way off, even if we wanted to. But it's nice that the option to slowly port to Chromium-based browsers who maintain support for Manifest V2 is there.
For those of you who want to talk about Tridactyl to other people, you'll be happy to know that docs are now built and hosted [on our website](https://tridactyl.xyz/build/static/docs/modules/_src_excmds_.html). I find I use it a lot when helping people on Discord/IRC/Matrix, maybe you will too.
## Neat bug fixes
We've finally fixed a feature that had been broken in Tridactyl for years - the grey hints for elements that have Javascript event handlers associated with them now work again. On some sites, this means that there are lots of duplicated hints - just run `:hint -J` (with perhaps `:bindurl ... f hint -J`) to exclude them from the displayed hints.
`:guiset gui none` works again in Firefox 133 - you may find that you need to rerun it.
In an apparent attempt to make me hate React even more than I already did, server-side rendering on React and Next.js started vandalising Tridactyl. I had to write a workaround so that Tridactyl reinserts itself when it detects that server-side React is running. Our detection isn't foolproof so may need to use `:seturl ... commandlineterriblewebsitefix true` to force Tridactyl to reinsert itself proactively. Since a fair number of the readers of this newsletter are probably web-developers, this might be a good time to remind you all that [friends don't let friends use React](https://infrequently.org/2024/11/if-not-react-then-what/). There are plenty of modern approaches to web development that don't spend their time reimplementing basic browser features, like Svelte or Lit. I have to say that my opinions on this hardened slightly after I was forced to write some code to deal with React tearing up the whole web page because the user agent dared to make some modifications to it.
This one probably only affects a small number of you, but `:apropos` will now work even if you have created a custom setting, which is important, because it's a great command for exploring Tridactyl :)
A bug that had been annoying me for most of the life of Tridactyl is that keypresses typed too quickly after a command opened the commandline would be lost. We now buffers character keypresses which has improved this a lot (although we can still lose some keys before the buffer starts collecting them, depending on how quick your computer is).
Most `searchurls` have been removed because people (especially me) found them annoying, you can add them back with `:set searchurls.[name] [url]` if there are any that you find yourself missing.
Finally, and most importantly, I added my [mastodon account](https://masto.ai/@bovine3dom) to the new tab page so that people can listen to me complain about about trains.
## Plans for next few months (years?)
I don't have anything terribly exciting planned for Tridactyl at the moment - we've got 82 pull requests open at the time of writing which is about 70 more than I would like, so I think I'll start trying to go through them and see what can be merged or freshened up and what should be closed. It's been a long time since I picked issues out at random to fix them so I'll probably set aside a weekend or two for that soon. At the back of my mind, I always think about rewriting our completion/commandline to be more deterministic and more configurable but the trouble is that it kind of works OK so it doesn't seem worth the time required to get it going. I would also like to fix up the ["keyup" bind PR](https://github.com/tridactyl/tridactyl/pull/4727) since that would enable real smooth scrolling which seems to be one of the main barriers to people using Tridactyl, but keybinds are such a key part of Tridactyl that the idea of touching that code and breaking it slightly is honestly a little bit terrifying. And then finally I still have [godmode](https://github.com/tridactyl/godmode) on my wishlist, which should get Tridactyl running on PDF and other pages like Firefox's own extensions such as the screenshot tool, for people who are prepared to patch Firefox.
As always, thanks for your generous support,
bovine3dom and the rest of the Tridactyl developers

View file

@ -8,10 +8,22 @@ If changing one of these settings fixes your bug, please visit the corresponding
- `:seturl $URL_OF_THE_WEBSITE superignore true` and then reload the page. This totally disables Tridactyl from loading on the page. No specific issue, please make a new one: https://github.com/tridactyl/tridactyl/issues/821
- "i can't open the commandline :((" `:seturl [the website you're on] commmandlineterriblewebsitefix true`. No need to file an issue with us, but maybe consider telling the website owner that they should [make their website less bad](https://infrequently.org/2024/11/if-not-react-then-what/). Our relevant issue is [#5050](https://github.com/tridactyl/tridactyl/issues/5050)
# Firefox settings that can break Tridactyl
If you have `privacy.resistFingerprinting` set to `true` in `about:config`, Tridactyl will have a lot of trouble understanding your keypresses. See [#760](https://github.com/tridactyl/tridactyl/issues/760#issuecomment-433679201) and [#1699](https://github.com/tridactyl/tridactyl/issues/1699). We strongly recommend setting it to `false`, as it is by default.
Selecting **Never remember history** in the History section of Firefox's Privacy & Security settings enables permanent private browsing. Tridactyl then treats every window as private and cannot persist state such as command history or global marks. Choose another history setting unless this behaviour is intended.
# Keyboard layout issues
Tridactyl's completion-aware `<Space>` binding can interfere with dead-key composition in the command line. Run `:unbind --mode=ex <Space>` to let Firefox handle Space normally, then `:bind --mode=ex <S-Space> ex.insert_character_or_completion` to move completion to Shift-Space. [#5061](https://github.com/tridactyl/tridactyl/issues/5061)
# RC file issues
If `:source` loads unexpected settings or does not reflect your changes, run `:findrc` to display the local RC file selected when no path is supplied. Check that path for an old or duplicate RC file.
# Native Editor/Messenger issues
If you're having trouble running your editor on OSX, you might be having \$PATH issues: [#684](https://github.com/tridactyl/tridactyl/issues/684). The solution is to specify the absolute path to your editor, like this: `:set editorcmd /usr/local/bin/vimr`.

View file

@ -8,6 +8,7 @@ import {
getDriver,
getDriverAndProfileDirs,
iframeLoaded,
quitDrivers,
sendKeys,
} from "./utils"
@ -22,9 +23,7 @@ describe("webdriver", () => {
driver = await getDriver()
})
afterEach(async () => {
await driver.quit()
})
afterEach(quitDrivers)
interface Tab {
active: boolean
@ -124,11 +123,14 @@ describe("webdriver", () => {
"elem.innerText=`%u`;" +
"document.body.appendChild(elem)<CR>",
)
await driver.executeScript(`
["news/rss.xml", "views/atom.xml", "pews/rss.xml", "tews/atom.xml"].forEach(href => {
const link = document.createElement("a")
link.href = href
document.body.appendChild(link)
})`)
// First, make sure completions are offered
await driver.get(
"file:///" + process.cwd() + "/e2e_tests/html/rss.html",
)
const iframe = await iframeLoaded(driver)
await sendKeys(driver, ":rssexec ")
await driver.switchTo().frame(iframe)
@ -196,8 +198,8 @@ describe("webdriver", () => {
const { driver, newProfiles } = await getDriverAndProfileDirs()
try {
// Then, make sure `:guiset` is offering completions
const iframe = await iframeLoaded(driver)
await sendKeys(driver, ":guiset ")
const iframe = await iframeLoaded(driver)
await driver.switchTo().frame(iframe)
const elements = await driver.findElements(
By.className("GuisetCompletionOption"),
@ -212,7 +214,7 @@ describe("webdriver", () => {
`return document.getElementById("tridactyl-input").value`,
),
).toEqual(
"userChrome.css written. Please restart Firefox to see the changes.",
`userChrome.css written to ${newProfiles[0]}/chrome/userChrome.css. Please restart Firefox to see the changes.`,
)
const profile = newProfiles.find(async p =>
(await fs.readdir(path.join(p, "chrome"))).find(files =>
@ -222,8 +224,6 @@ describe("webdriver", () => {
expect(profile).toBeDefined()
} catch (e) {
fail(e)
} finally {
await driver.quit()
}
})
@ -368,7 +368,7 @@ describe("webdriver", () => {
await untilTabUrlMatches(
driver,
newTab.id,
new RegExp("^https://www.google.com/search.*qwant"),
new RegExp("^https?:\/\/.*qwant", "i"),
)
} catch (e) {
fail(e)

View file

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tridactyl RSS test page</title>
</head>
<body>
<a href="news/rss.xml">
<a href="views/atom.xml">
<a href="pews/rss.xml">
<a href="tews/atom.xml">
</body>
</html>

View file

@ -3,9 +3,10 @@ import * as path from "path"
import * as os from "os"
import * as process from "process"
import { Browser, Builder, By, Key, WebDriver } from "selenium-webdriver"
import { Driver, Options } from "selenium-webdriver/firefox"
import { Driver, Options, ServiceBuilder } from "selenium-webdriver/firefox"
import * as Until from "selenium-webdriver/lib/until"
const env = process.env
const drivers = new Set<Driver>()
/** Returns the path of the newest file in directory */
export async function getNewestFileIn(directory: string): Promise<string> {
@ -41,26 +42,41 @@ export async function getDriver() {
)
const options = new Options()
if (env["HEADLESS"]) {
if (!env["HEADED"]) {
options.addArguments("--headless")
}
const driver = new Builder()
.forBrowser(Browser.FIREFOX)
.setFirefoxOptions(options)
// Required to evaluate scripts in extension pages; only for test browsers.
.setFirefoxService(new ServiceBuilder().addArguments("--allow-system-access"))
.build() as unknown as Driver
drivers.add(driver)
// This will be the default tab.
await driver.installAddon(extensionPath, true)
// Wait until addon is loaded and :tutor is displayed
await iframeLoaded(driver)
const handles = await driver.getAllWindowHandles()
// And wait a bit more otherwise Tridactyl won't be happy
// Wait for multiple window handles to be available (extension may open in new tab)
await driver.wait(async () => {
const handles = await driver.getAllWindowHandles()
return handles.length >= 2
}, 10000)
// Give the extension a bit more time to initialize
await driver.sleep(500)
let handles = await driver.getAllWindowHandles()
// Handle edge case where extension loads in same tab (some headless configurations)
if (handles.length === 1) {
await driver.wait(async () => {
const newHandles = await driver.getAllWindowHandles()
return newHandles.length >= 2
}, 10000)
handles = await driver.getAllWindowHandles()
}
// Kill the original tab.
await driver.switchTo().window(handles[0])
await driver.close()
// Switch back to the good tab.
// Switch to the new tab (extension opens in new tab)
await driver.switchTo().window(handles[1])
await driver.wait(() => driver.executeScript<boolean>("return Boolean(window.tri)"), 10000)
// Now return the window that we want to use.
return driver
}
@ -83,6 +99,18 @@ export async function getDriverAndProfileDirs() {
return { driver, newProfiles }
}
export async function quitDrivers() {
const results = await Promise.allSettled(
[...drivers].map(driver =>
driver.quit().finally(() => drivers.delete(driver)),
),
)
const failure = results.find(result => result.status === "rejected")
if (failure?.status === "rejected") {
throw failure.reason
}
}
const vimToSelenium = {
Down: Key.ARROW_DOWN,
Left: Key.ARROW_LEFT,

111
funding.json Normal file
View file

@ -0,0 +1,111 @@
{
"$schema": "https://fundingjson.org/schema/v1.1.0.json",
"version": "v1.1.0",
"entity": {
"type": "individual",
"role": "owner",
"name": "Oliver Blanthorn",
"email": "freedom4cows@gmail.com",
"description": "Creator and maintainer of Tridactyl, a popular vim-like web extension for Firefox, and various open-data/geospatial projects.",
"webpageUrl": {
"url": "https://github.com/bovine3dom"
}
},
"projects": [
{
"guid": "tridactyl",
"name": "Tridactyl",
"description": "A popular vim-like web extension for Firefox, spiritual successor to Vimperator/Pentadactyl",
"webpageUrl": {
"url": "https://github.com/tridactyl/tridactyl"
},
"repositoryUrl": {
"url": "https://github.com/tridactyl/tridactyl"
},
"licenses": ["spdx:Apache-2.0"],
"tags": ["browser-extensions", "developer-tools"]
}
],
"funding": {
"channels": [
{
"guid": "github",
"type": "payment-provider",
"address": "https://github.com/bovine3dom/sponsors",
"description": "For most people, the most efficient way of funding me - Microsoft covers all card/exchange fees"
},
{
"guid": "liberapay",
"type": "payment-provider",
"address": "https://liberapay.com/bovine3dom"
},
{
"guid": "bank",
"type": "cash",
"address": "freedom4cows@gmail.com",
"description": "Contact me directly for my IBAN/BIC"
},
{
"guid": "revolut",
"type": "payment-provider",
"address": "https://revolut.me/oab"
},
{
"guid": "crypto",
"type": "other",
"address": "freedom4cows@gmail.com",
"description": "Contact me directly for crypto donations because there are too many different coins to list :)"
},
{
"guid": "paypal",
"type": "payment-provider",
"address": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY",
"description": "NB: I only receive ~80% of the income after fees"
},
{
"guid": "patreon",
"type": "payment-provider",
"address": "https://patreon.com/tridactyl",
"description": "NB: I only receive ~60% of the income after fees and forced VAT"
}
],
"plans": [{
"guid": "feedme",
"status": "active",
"name": "Monthly donations",
"description": "Most of my living costs are monthly and so monthly donations massively help me to plan",
"amount": 0,
"currency": "EUR",
"frequency": "monthly",
"channels": ["github", "liberapay", "bank", "crypto", "revolut", "paypal", "patreon"]
}],
"history": [
{
"year": 2023,
"income": 4200,
"taxes": 1000,
"currency": "EUR",
"description": "Approximate"
},
{
"year": 2024,
"income": 6500,
"taxes": 1500,
"currency": "EUR",
"description": "Approximate"
},
{
"year": 2025,
"income": 5800,
"taxes": 1400,
"currency": "EUR",
"description": "Approximate"
}
]
}
}

View file

@ -14,12 +14,15 @@ source ./scripts/common.sh
jsfiles=$(cachedTSLintFiles)
otherfiles=$(cachedPrettierFiles)
# Exit early if no relevant files are staged
if [ -z "$jsfiles" ] && [ -z "$otherfiles" ]; then
exit 0
fi
echo "Running pre-commit hook..."
# Check if any of the files are ugly or contain a console.log call
consoleFiles=$(noisy $jsfiles)
uglyFiles="$(eslintUgly $jsfiles)"
if [ ! -n "$uglyFiles" ]; then
uglyFiles="$(prettierUgly $otherfiles)"
fi
if [ -n "$consoleFiles" ]; then
echo "Warning: adding console.log calls in ${consoleFiles[@]}"
@ -27,10 +30,26 @@ if [ -n "$consoleFiles" ]; then
echo
fi
if [ -n "$uglyFiles" ]; then
echo "Prettify your files first:"
echo 'yarn run pretty'
echo ''
echo 'If you see this message repeatedly, skip the check with git commit -n'
exit 1
# # disabled because it was too noisy
#
# if [ -n "$jsfiles" ]; then
# yarn git-format-staged \
# --formatter "yarn --silent prettier --stdin-filepath \"{}\"" \
# "*.ts" "*.tsx"
# fi
# if [ -n "$otherfiles" ]; then
# yarn git-format-staged \
# --formatter "yarn --silent prettier --stdin-filepath \"{}\"" \
# "*.md" "*.css"
# fi
if [ -n "$jsfiles" ]; then
echo "Linting staged files..."
if ! yarn eslint --rulesdir custom-eslint-rules --quiet $jsfiles; then
echo ""
echo -e "eslint failed. Please fix the errors above."
echo 'If you see this message repeatedly, skip the check with git commit -n'
exit 1
fi
fi

View file

@ -1,7 +1,8 @@
const tsConfig = require('./tsconfig');
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
testRunner: "jest-jasmine2",
setupFiles: [
"jest-webextension-mock"
],
@ -9,16 +10,19 @@ module.exports = {
"./e2e_tests/failfast.js"
],
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
globals: {
"ts-jest": {
tsConfig: {
...tsConfig.compilerOptions,
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
tsconfig: {
...tsConfig.compilerOptions,
types: ["jest", "node", "@types/firefox-webext-browser"]
},
diagnostics: {
ignoreCodes: [151001]
},
}
},
diagnostics: {
ignoreCodes: [151001]
}
}
]
},
moduleNameMapper: {
"@src/(.*)": "<rootDir>/src/$1"

View file

@ -13,52 +13,52 @@
},
"dependencies": {
"@mozilla/readability": "^0.5.0",
"cleanslate": "^0.10.1",
"cleanslate": "git+https://github.com/tridactyl/cleanslate.git#master",
"compute-scroll-into-view": "^3.0.0",
"csp-serdes": "github:cmcaine/csp-serdes",
"css": "^3.0.0",
"editor-adapter": "^0.0.5",
"editor-adapter": "git+https://github.com/tridactyl/editor-adapter.git#master",
"esbuild": "^0.20.2",
"fuse.js": "^7.0.0",
"nearley": "^2.20.1",
"ramda": "^0.30.1",
"reading-time": "^1.5.0",
"semver-compare": "^1.0.0",
"stream-browserify": "^3.0.0",
"tridactyl-arg": "git+https://github.com/GHolk/arg.git#v5.1.0",
"tsdef": "^0.0.14",
"typedoc": "^0.19.2",
"typedoc-default-themes": "^0.12.10",
"typedoc": "0.28.20",
"xss": "^1.0.15"
},
"devDependencies": {
"@types/css": "0.0.37",
"@types/css": "0.0.38",
"@types/firefox-webext-browser": "^120.0.4",
"@types/jest": "^27.5.0",
"@types/jest": "29.5.12",
"@types/nearley": "^2.11.5",
"@types/selenium-webdriver": "^4.1.10",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/eslint-plugin-tslint": "^6.21.0",
"@typescript-eslint/parser": "^4.33.0",
"command-line-args": "^5.2.1",
"eslint": "^7.32.0",
"@typescript-eslint/eslint-plugin": "8.64.0",
"@typescript-eslint/parser": "8.64.0",
"command-line-args": "^6.0.1",
"eslint": "8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.2.1",
"eslint-plugin-jsdoc": "^50.6.1",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-sonarjs": "^0.25.1",
"geckodriver": "^4.3.3",
"geckodriver": "^5.0.0",
"git-format-staged": "^4.0.1",
"jasmine-fail-fast": "^2.0.1",
"jest": "^25.5.4",
"jest-webextension-mock": "^3.9.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-jasmine2": "29.7.0",
"jest-webextension-mock": "^4.0.0",
"marked": "^12.0.1",
"prettier": "^3.2.5",
"prettier": "^3.4.2",
"selenium-webdriver": "^4.7.1",
"ts-jest": "^25.5.1",
"tslint": "^5.20.1",
"tslint-etc": "^1.13.10",
"tslint-sonarts": "^1.9.0",
"typescript": "^3.9.10",
"web-ext": "^7.10.0"
"ts-jest": "29.4.11",
"typescript": "6.0.3",
"web-ext": "^7.10.0",
"yaml-lint": "^1.7.0"
},
"scripts": {
"build": "sh scripts/build.sh",
@ -70,10 +70,14 @@
"make-zip": "web-ext build --source-dir build --overwrite-dest",
"pretty": "bash scripts/pretty.sh",
"run": "web-ext run -s build/ -u 'paste.to'",
"runwithprofile": "node scripts/runwithprofile.js",
"test": "yarn run build && web-ext build --source-dir ./build --overwrite-dest && jest --silent",
"update-buildsystem": "rm -rf src/node_modules; yarn run clean",
"watch": "echo 'watch is broken, use build instead'; exit 0;",
"install": "git config core.hookspath hooks/"
"install": "git config core.hookspath hooks/",
"_dev": "yarn run rebuild && yarn run run",
"dev": "ls **/*.ts | entr -r yarn run _dev",
"githublint": "yamllint .github/workflows/**.yml"
},
"author": "Colin Caine",
"repository": {
@ -90,5 +94,6 @@
"bugs": {
"url": "https://github.com/tridactyl/tridactyl/issues"
},
"homepage": "https://github.com/tridactyl/tridactyl#readme"
"homepage": "https://github.com/tridactyl/tridactyl#readme",
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View file

@ -9,7 +9,6 @@ Tridactyl
<h4 align="center">Replace Firefox's default control mechanism with one modelled on the one true editor, Vim.</h4>
<p align="center">
<a href="https://travis-ci.org/tridactyl/tridactyl"><img src="https://travis-ci.org/tridactyl/tridactyl.svg?branch=master" alt="Build Status"></a>
<a href="https://matrix.to/#/#tridactyl:matrix.org"><img src="https://img.shields.io/badge/matrix-join%20chat-green" alt="Matrix Chat"></a>
<a href="https://gitter.im/tridactyl/Lobby"><img src="https://badges.gitter.im/Join%20Chat.svg" alt="Join Gitter Chat"></a>
<a href="https://discord.gg/DWbNGTAvmh"><img src="https://img.shields.io/discord/854326924402622474?color=%235865F2&label=discord" alt="Join Discord Chat"></a>
@ -53,7 +52,7 @@ Tridactyl stable can be installed from the [Mozilla add-ons website (the AMO)][a
### Extra features through [Native Messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging)
If you want to use advanced features such as edit-in-Vim, you'll also need to install the native messenger or executable, instructions for which can be found by typing `:installnative` and hitting enter once you are in Tridactyl. Arch users can install the [AUR package](https://aur.archlinux.org/packages/firefox-tridactyl-native/) `firefox-tridactyl-native` instead.
If you want to use advanced features such as edit-in-Vim, you'll also need to install the native messenger or executable, instructions for which can be found by typing `:nativeinstall` and hitting enter once you are in Tridactyl. Arch users can install the [AUR package](https://aur.archlinux.org/packages/firefox-tridactyl-native/) `firefox-tridactyl-native` instead.
#### Containerized/sandboxed Firefox Installations
@ -81,7 +80,7 @@ The changelog for the stable versions can be found [here](https://github.com/tri
Type `:help` or press `<F1>` for online help once you're in, or `:tutor` for a friendly introduction. You might also find the [unofficial Tridactyl Memrise course](https://app.memrise.com/community/course/5995499/tridactyls-main-shortcuts/) (requires login) useful for memorising keybinds.
Remember that Tridactyl cannot run on any page on about:\*, data:\*, view-source:\* and file:\*. We're sorry about that and we're working with Firefox to improve this situation by removing restrictions on existing APIs and developing a new API.
Remember that Tridactyl cannot run on any page on about:\*, data:\*, view-source:\* and file:\*. We're sorry about that :(
If you're enjoying Tridactyl, or not, please leave a review on the [AMO](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/reviews/).
@ -111,11 +110,11 @@ You can try `:help key` to know more about `key`. If it is an existing binding,
- `gi` — scroll to and focus the last-used input on the page
- `r`/`R` — reload page or hard reload page
- `yy` — copy the current page URL to the clipboard
- `[[`/`]]` — navigate forward/backward though paginated pages, for example comics, multi-part articles, search result pages, etc.
- `[[`/`]]` — navigate backward/forward though paginated pages, for example comics, multi-part articles, search result pages, etc.
- `]c`/`[c` — increment/decrement the current URL by 1
- `gu` — go to the parent of the current URL
- `gU` — go to the root domain of the current URL
- `gr` — open Firefox reader mode (note: Tridactyl will not work in this mode)
- `gr` — open Firefox reader mode (note: Tridactyl will only work on our own `:reader` command which has complicated security implications)
- `zi`/`zo`/`zz` — zoom in/out/reset zoom
- `<C-f>`/`<C-b>` — jump to the next/previous part of the page
- `g?` — Apply Caesar cipher to page (run `g?` again to switch back)
@ -162,7 +161,7 @@ If you want to use Firefox's default `<C-b>` binding to open the bookmarks sideb
- `u` — undo the last tab/window closure
- `gt`/`gT` — go to the next/previous tab
- `g^ OR g0`/`g$` — go to the first/last tab
- `ga` — go to the tab currently playing audio
- `ga` — go to the tab currently playing audio, or the one that most recently stopped
- `<C-^>` — go to the last active tab
- `b` — bring up a list of open tabs in the current window; you can type the tab ID or part of the title or URL to choose a tab
@ -185,7 +184,7 @@ Additionally, you can hint elements matching a custom CSS selector with `:hint -
### Binding custom commands
You can bind your own shortcuts in normal mode with the `:bind` command. For example `:bind J tabprev` to bind `J` to switch to the previous tab. See `:help bind` for details about this command.
You can bind your own shortcuts in normal mode with the `:bind` command. For example `:bind gD composite tabduplicate; tabdetach` to duplicate and detach the current tab. See `:help bind` for details about this command.
## WebExtension-related issues
@ -210,6 +209,10 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
`:set searchengine esa`
Note that this does not apply to `:open` when run without arguments, as it simply opens the newtab page which can be set like this:
`:set newtab [newtab url]`, e.g. `:set newtab https://www.google.com`
- How can I add a search engine?
`:set searchurls.esa http://www.esa.int/esasearch?q=`
@ -222,7 +225,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
- Can I import/export settings, and does Tridactyl use an external configuration file just like Vimperator?
Yes. `:source --url [URL]` accepts a URL (which must contain only an RC file, e.g. `raw.githubusercontent.com/...`). If you have `native` working, `$XDG_CONFIG_HOME/tridactyl/tridactylrc` or `~/.tridactylrc` will be read at startup via an `autocmd` and `source`. There is an [example file available on our repository](https://github.com/tridactyl/tridactyl/blob/master/.tridactylrc).
Yes. `:source --url [URL]` accepts a URL (which must contain only an RC file, e.g. `raw.githubusercontent.com/...`). If you have `native` working, `$XDG_CONFIG_HOME/tridactyl/tridactylrc` or `~/.tridactylrc` will be read at startup via an `autocmd` and `source`. Run `:findrc` to see which local RC file this automatic search selected. There is an [example file available on our repository](https://github.com/tridactyl/tridactyl/blob/master/.tridactylrc), or you can [search GitHub for other RC files](https://github.com/search?q=path%3Atridactylrc&type=code) (requires login).
There's also `mkt` which exports your configuration to a file which may be read with `source`. (NB: this currently requires `native`).
@ -231,6 +234,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
To use one of the built in themes use: `:colors <color>`. The current options are:
- default
- auto
- dark (authored by @furgerf)
- shydactyl (authored by @atrnh)
- greenmat (authored by @caputchinefrobles)
@ -238,12 +242,14 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
- quake
- quakelight
- midnight (authored by @karizma)
- vimium
- tokyonight
Tridactyl can also load themes from disk or URL. You could use this for example to load one of the themes originally authored by @bezmi ([tridactyl/base16-tridactyl](https://github.com/tridactyl/base16-tridactyl)). See `:help colors` for more information.
- How to remap keybindings? or How can I bind keys using the control/alt key modifiers (eg: `ctrl+^`)?
You can remap keys in normal, ignore, input and insert mode with `:bind --mode=$mode $key $excmd`. Hint mode and the command line are currently special and can't be rebound. See `:help bind` for more information.
You can remap keys with `:bind --mode=$mode $key $excmd`. See `:help bind` for more information.
Modifiers can be bound like this: `:bind <C-f> scrollpage 1`. Special keys can be bound too: `:bind <F3> colors dark` and with modifiers: `:bind <S-F3> colors default` and with multiple modifiers: `:bind <SA-F3> composite set hintchars 1234567890 | set hintfiltermode vimperator-reflow`
@ -275,7 +281,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
- Can I change proxy via commands?
Not yet, but this feature will eventually be implemented.
Yes, see `:help proxyadd`
- How do I disable Tridactyl on certain sites?
@ -285,7 +291,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
- How can I list the current bindings?
`viewconfig nmaps` works OK, but Tridactyl commands won't work on the shown page for "security reasons". We'll eventually provide a better way. See [#98](https://github.com/tridactyl/tridactyl/issues/98).
`viewconfig nmaps` works OK, but Tridactyl commands won't work on the shown page for "security reasons". We'll eventually provide a better way. See [#98](https://github.com/tridactyl/tridactyl/issues/98). You can also look at `:bind ` completions and `:apropos `
- How can I know which mode I'm in/have a status line?
@ -293,7 +299,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
- Does anyone actually use Tridactyl?
In addition to the developers, some other people do. Mozilla keeps tabs on stable users [here](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/statistics/?last=30), but, as of a while ago, you can't see that link if you aren't listed as a Tridactyl developer on the AMO. The maintainers guess the number of unstable users from unique IPs downloading the betas each week when they feel like it. Last time they checked there were 4600 of them.
In addition to the developers, some other people do. Mozilla keeps tabs on stable users [here](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/statistics/?last=30), but, as of a while ago, you can't see that link if you aren't listed as a Tridactyl developer on the AMO. The maintainers guess the number of unstable users from unique IPs downloading the betas each week when they feel like it. Last time they checked there were 4600 of them. Unscientifically extrapolating from Arch linux's pop contest for package installations gives us an estimate of around 50,000 users.
- How do I prevent websites from stealing focus?
@ -303,7 +309,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
### Donations
We gratefully accept donations via [GitHub Sponsors](https://github.com/users/bovine3dom/sponsorship) (we receive 100% of your donation), [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY) (about 70% of your donation makes it to our bank account after fees) and [Patreon](https://www.patreon.com/tridactyl) (about 70% of your donation makes it to our account). If you can, please make this a monthly donation as it makes it much easier to plan. People who donate more than 10USD a month via GitHub or Patreon get a special monthly "tips and tricks" newsletter - see an example [here](https://github.com/tridactyl/tridactyl/blob/master/doc/newsletters/tips-and-tricks/1-hint-css-selectors.md). All GitHub and Patreon donors get a quarterly newsletter on Tridactyl development.
We gratefully accept donations via [GitHub Sponsors](https://github.com/users/bovine3dom/sponsorship) (we receive 100% of your donation), [Liberapay](https://liberapay.com/bovine3dom/) (about 95% of your donation makes it to our account), [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY) (about 70% of your donation makes it to our bank account after fees) and [Patreon](https://www.patreon.com/tridactyl) (about 70% of your donation makes it to our account). If you can, please make this a monthly donation as it makes it much easier to plan. People who donate more than 10USD a month via GitHub or Patreon get a special monthly "tips and tricks" newsletter - see an example [here](https://github.com/tridactyl/tridactyl/blob/master/doc/newsletters/tips-and-tricks/1-hint-css-selectors.md). All GitHub and Patreon donors get a quarterly newsletter on Tridactyl development.
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY"><img src="https://www.paypalobjects.com/en_US/GB/i/btn/btn_donateCC_LG.gif" alt="PayPal"></a>
@ -343,6 +349,8 @@ If you want to build a signed copy (e.g. for the non-developer release), you can
You can build unsigned copies with `scripts/sign nosign{stable,beta}`. NB: The `stable` versus `beta` part of the argument tells our build process which extension ID to use (and therefore which settings to use). If you want a stable build, make sure you are on the latest tag, i.e. `git checkout $(git tag | grep '^[0-9]\+\.[0-9]\+\.[0-9]\+$' | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -1)`.
Maintainers create the next release after fetching tags with `scripts/version.js next {0,1,2} [release name]` and committing the changed manifest. Beta builds use that version while retaining the release name in their displayed version. Once its changelog entry is ready, `scripts/version.js release` dates the entry, commits it, and tags the version already in `src/manifest.json`.
If you are on a distribution which builds Firefox with `--with-unsigned-addon-scopes=` set to `app` and/or `system` (which is most of them by users: Arch, Debian, Ubuntu), you can install your unsigned copy of Tridactyl with `scripts/install.sh [directory]`. If you're on Arch, the correct directory is probably selected by default; on other distributions you might have to go hunting, but it probably looks like `/usr/lib/firefox/browser/extensions`.
### Building on Windows
@ -351,13 +359,11 @@ If you are on a distribution which builds Firefox with `--with-unsigned-addon-sc
- Install [NodeJS for Windows][win-nodejs]
- Current 8.11.1 LTS seems to work fine
- Launch the installation steps described above from MinTTY shell
- Also known as "Git Bash"
[win-git]: https://git-scm.com/download/win
[win-nodejs]: https://nodejs.org/dist/v8.11.1/node-v8.11.1-x64.msi
[win-nodejs]: https://nodejs.org/dist/
[pyinstaller]: https://www.pyinstaller.org
[gpg4win]: https://www.gpg4win.org
@ -375,7 +381,7 @@ You can speed up the build process after your first build by using `yarn run reb
### Committing
A pre-commit hook is added by `yarn install` that simply runs `yarn test`. If you know that your commit doesn't break the tests you can commit with `git commit -n` to ignore the hooks. If you're making a PR, travis will check your build anyway.
A pre-commit hook is added by `yarn install` that simply runs `yarn test`. If you know that your commit doesn't break the tests you can commit with `git commit -n` to ignore the hooks. If you're making a PR, GitHub will check your build anyway once a maintainer has approved it.
### Documentation

View file

@ -12,7 +12,7 @@ authors="../../build/static/authors.html"
sed "/REPLACETHIS/,$ d" authors.html > "$authors"
# If we're in a git repo, refresh the cache
if [ -d "../../.git/" ]; then
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git shortlog -sn HEAD | cut -c8- | awk '!seen[$0]++' | sed 's/^/<p>/' | sed 's/$/<\/p>/' > ../../.build_cache/authors
fi

View file

@ -42,6 +42,7 @@ mkdir -p build
mkdir -p build/static
mkdir -p generated/static
mkdir -p generated/static/clippy
node scripts/make_glossary.js
if [ "$(isWindowsMinGW)" = "True" ]; then
$WIN_PYTHON scripts/excmds_macros.py
@ -57,13 +58,19 @@ if [ "$QUICK_BUILD" != "1" ]; then
"$(yarn bin)/nearleyc" src/grammars/bracketexpr.ne > \
src/grammars/.bracketexpr.generated.ts
# It's important to generate the metadata before the documentation because
# missing imports might break documentation generation on clean builds
"$(yarn bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2017 \
&& node compiler/gen_metadata.js \
--out src/.metadata.generated.ts \
--themeDir src/static/themes \
src/excmds.ts src/lib/config.ts
printf '{"version":1,"commands":{},"settings":{}}' > src/.metadata.generated.json
printf 'export * from "./lib/metadata"\n' > src/.metadata.generated.ts
node -e "var fs=require('fs');fs.writeFileSync('src/.themes.generated.json',JSON.stringify(fs.readdirSync('src/static/themes').sort()))"
# Generate runtime metadata via typedoc. The generated TypeScript shim
# routes the public @src/.metadata.generated import to the JSON loader.
"$(yarn bin)/typedoc" --json src/.metadata.generated.json \
--excludeExternals --disableSources --readme none \
--excludePrivate false --excludePrivateClassFields false \
--validation.notExported false \
--exclude 'src/.excmds_*.generated.ts' \
src/excmds.ts src/lib/config.ts src/content/state_content.ts
node scripts/convert_typedoc_metadata.js src/.metadata.generated.json
scripts/newtab.md.sh
scripts/make_tutorial.sh
@ -85,7 +92,7 @@ rmdir buildtemp
# Copy extra static files across
cp src/manifest.json build/
node scripts/generate_manifest.js firefox src/manifest.json build/manifest.json
cp -r src/static build
cp -r generated/static build
cp issue_template.md build/

View file

@ -12,51 +12,12 @@ cachedPrettierFiles() {
git diff --cached --name-only --diff-filter=ACM "*.md" "*.css"
}
# Accepts a single argument which is the name of a file tracked by git
# Returns a string which is the content of the file as stored in the git index
staged() {
git show :"$1"
}
# Accepts a single string argument made of multiple file names separated by a newline
# Returns an array of files that prettier wants to lint
prettierUgly() {
local acc=""
local IFS=$'\n'
for jsfile in $1; do
diff <(staged "$jsfile") <(staged "$jsfile" | "$(yarn bin)/prettier" --stdin-filepath "$jsfile") >/dev/null || acc="$jsfile"$'\n'"$acc"
done
echo "$acc"
}
eslintUgly() {
local acc=""
local IFS=$'\n'
local tmpdir
mkdir -p ".tmp"
if [[ "$(uname)" == "Darwin" ]]; then
tmpdir=$(gmktemp --tmpdir=".tmp/" -d "tslint.XXXXXXXXX")
else
tmpdir=$(mktemp --tmpdir=".tmp/" -d "tslint.XXXXXXXXX")
fi
for jsfile in "$@"; do
tmpfile="$tmpdir/$jsfile"
mkdir -p "$(dirname "$tmpfile")"
staged "$jsfile" > "$tmpfile"
"$(yarn bin)/eslint" --rulesdir custom-eslint-rules --no-ignore --quiet -o /dev/null "$tmpfile" || acc="$jsfile"$'\n'"$acc"
done
rm -rf "$tmpdir"
echo "$acc"
}
noisy() {
local acc=()
local acc=""
for jsfile in "$@"; do
if [ "$(git diff --cached "$jsfile" | grep '^+.*console.log' -c)" -gt '0' ] ; then
acc+=("jsfile")
acc+="$acc$jsfile"$'\n'
fi
done
echo "${acc[@]}"
echo "$acc"
}

View file

@ -0,0 +1,210 @@
#!/usr/bin/env node
const fs = require("fs")
const TYPEDOC_SCHEMA_VERSION = "2.0"
const METADATA_VERSION = 1
const KIND = { Module: 2, Function: 64, Class: 128 }
const compareNames = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
const sortedRecord = entries =>
Object.fromEntries(entries.sort(([a], [b]) => compareNames(a, b)))
function convertMetadata(project) {
if (project.schemaVersion !== TYPEDOC_SCHEMA_VERSION)
throw new Error(
`Unsupported TypeDoc schemaVersion ${JSON.stringify(project.schemaVersion)}; expected ${TYPEDOC_SCHEMA_VERSION}`,
)
const reflections = {}
const collect = value => {
if (!value || typeof value !== "object") return
if (Array.isArray(value)) return value.forEach(collect)
if (value.id !== undefined) reflections[value.id] = value
Object.values(value).forEach(collect)
}
collect(project)
const accessor = node => node?.getSignature || node?.setSignature
const memberType = node => node?.type || accessor(node)?.type
const commentText = comment =>
(comment?.summary || [])
.map(part => part.text || "")
.join("")
.replace(/\n+$/, "")
const argFlags = comment => {
const flags = {}
for (const tag of comment?.blockTags || []) {
if (tag.tag !== "@flag") continue
const text = (tag.content || [])
.map(part => part.text || "")
.join("")
const flagText = text.split(/\r?\n[ \t]*\r?\n/, 1)[0]
const m = /^(-\S+)[ \t]+([^\n]+)\n*([\s\S]*)$/.exec(flagText.trim())
if (!m) continue
const [, flag, short, rest] = m
const elaboration = rest.trim()
flags[flag] = {
short,
description: elaboration ? `${short}\n\n${elaboration}` : short,
}
}
return flags
}
const normalizeParameter = (parameter, resolving) => ({
name: parameter.name,
type: normalizeType(parameter.type, resolving),
...(parameter.flags?.isRest ? { flags: { isRest: true } } : {}),
})
const normalizeType = (type, resolving = new Set()) => {
if (!type) return { type: "intrinsic", name: "any" }
if (
type.type === "reference" &&
typeof type.target === "number" &&
!type.typeArguments?.length &&
reflections[type.target]?.type &&
!resolving.has(type.target)
) {
const next = new Set(resolving)
next.add(type.target)
return normalizeType(reflections[type.target].type, next)
}
switch (type.type) {
case "intrinsic":
return { type: "intrinsic", name: type.name }
case "array":
return {
type: "array",
elementType: normalizeType(type.elementType, resolving),
}
case "tuple":
return {
type: "tuple",
elements: (type.elements || []).map(element =>
normalizeType(element, resolving),
),
}
case "union":
return {
type: "union",
types: (type.types || []).map(member =>
normalizeType(member, resolving),
),
}
case "literal":
return { type: "literal", value: type.value }
case "reference":
return {
type: "reference",
name: type.name,
...(type.typeArguments?.length
? {
typeArguments: type.typeArguments.map(argument =>
normalizeType(argument, resolving),
),
}
: {}),
}
case "reflection": {
const source = type.declaration || {}
const declaration = {}
const signature = source.signatures?.[0]
if (signature)
declaration.signatures = [
{
parameters: (signature.parameters || []).map(
parameter =>
normalizeParameter(parameter, resolving),
),
type: normalizeType(signature.type, resolving),
},
]
const children = (source.children || [])
.map(child => {
const type = memberType(child)
return type
? {
name: child.name,
type: normalizeType(type, resolving),
}
: undefined
})
.filter(Boolean)
.sort((a, b) => compareNames(a.name, b.name))
if (children.length) declaration.children = children
const indexSignatures = (source.indexSignatures || []).map(
signature => ({
type: normalizeType(signature.type, resolving),
}),
)
if (indexSignatures.length)
declaration.indexSignatures = indexSignatures
return { type: "reflection", declaration }
}
default:
return { type: "intrinsic", name: "any" }
}
}
const modules = Object.fromEntries(
(project.children || [])
.filter(node => node.kind === KIND.Module)
.map(node => [
(node.name || "").replace(/^"|"$/g, "").replace(/\\/g, "/"),
node,
]),
)
const excmds = modules.excmds
const configClass = modules["lib/config"]?.children?.find(
node => node.kind === KIND.Class && node.name === "default_config",
)
if (!excmds || !configClass)
throw new Error("TypeDoc output is missing excmds or default_config")
const commands = sortedRecord(
(excmds.children || [])
.filter(node => node.kind === KIND.Function)
.map(node => {
const signature = node.signatures?.[0]
const comment = signature?.comment || node.comment
const flags = argFlags(comment)
return [
node.name,
{
doc: commentText(comment),
params: (signature?.parameters || []).map(parameter =>
normalizeParameter(parameter),
),
...(Object.keys(flags).length ? { flags } : {}),
},
]
}),
)
const settings = sortedRecord(
(configClass.children || []).map(node => [
node.name,
{
doc:
commentText(accessor(node)?.comment) ||
commentText(node.comment),
type: normalizeType(memberType(node)),
},
]),
)
return { version: METADATA_VERSION, commands, settings }
}
module.exports = { convertMetadata, METADATA_VERSION, TYPEDOC_SCHEMA_VERSION }
if (require.main === module) {
const file = process.argv[2]
if (!file) throw new Error("Usage: convert_typedoc_metadata.js FILE")
const metadata = convertMetadata(JSON.parse(fs.readFileSync(file, "utf8")))
fs.writeFileSync(file, JSON.stringify(metadata))
}

View file

@ -1,6 +1,6 @@
const esbuild = require('esbuild')
for (let f of ["content", "background", "help", "newtab", "reader", "commandline_frame", "qrCodeGenerator"]) {
for (let f of ["content", "background", "help", "newtab", "reader", "commandline_frame", "qrCodeGenerator", "browser_action_popup"]) {
esbuild.build({
entryPoints: [`src/${f}.ts`],
bundle: true,

View file

@ -201,5 +201,5 @@ def main():
print(output.rstrip(), file=sink)
PRELUDE = "/** Generated from excmds.ts. Don't edit this file! */"
PRELUDE = "/* Generated from excmds.ts. Don't edit this file! */"
main()

View file

@ -0,0 +1,30 @@
#!/usr/bin/env node
const fs = require("fs")
const targets = require("../browser-targets.json")
function generateManifest(template, targetName) {
const target = targets[targetName]
if (!target || !target.manifestVersionPath) {
throw new Error(`No manifest settings for ${targetName}`)
}
const manifest = JSON.parse(JSON.stringify(template))
const path = target.manifestVersionPath.slice()
const property = path.pop()
const parent = path.reduce((value, name) => {
if (!value[name]) value[name] = {}
return value[name]
}, manifest)
parent[property] = target.minimumVersion
return manifest
}
if (require.main === module) {
const [targetName, source, destination] = process.argv.slice(2)
const template = JSON.parse(fs.readFileSync(source, "utf8"))
const manifest = generateManifest(template, targetName)
fs.writeFileSync(destination, JSON.stringify(manifest, null, 4) + "\n")
}
module.exports = generateManifest

View file

@ -1,4 +1,24 @@
#!/bin/sh
set -e
dest=generated/static/docs
"$(yarn bin)/typedoc" --theme src/static/typedoc/ --exclude "src/**/?(test_utils|*.test).ts" --out $dest src --ignoreCompilerErrors
cp -r $dest build/static/
if revision=$(git rev-parse HEAD 2>/dev/null); then
set -- --basePath src --disableGit --gitRevision "$revision" \
--sourceLinkTemplate 'https://github.com/tridactyl/tridactyl/blob/{gitRevision}/src/{path}#L{line}'
else
set -- --disableSources
fi
"$(yarn bin)/typedoc" --plugin ./scripts/typedoc-theme.mjs \
--theme tridactyl --router tridactyl \
"$@" \
--entryPointStrategy expand --validation.notExported false \
--excludeInternal \
--excludePrivate false --excludePrivateClassFields false \
--includeHierarchySummary false \
--exclude "src/**/?(test_utils|*.test).ts" \
--out "$dest" src
for command in next_completion prev_history accept_line; do
grep -q "id=\"$command\"" "$dest/modules/_src_commandline_frame_.html"
done
rm -rf build/static/docs
cp -r "$dest" build/static/

76
scripts/make_glossary.js Normal file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env node
const fs = require("fs")
const path = require("path")
const root = path.resolve(__dirname, "..")
const source = path.join(root, "doc/glossary.jsonl")
const entries = []
const anchors = new Set()
for (const [index, line] of fs
.readFileSync(source, "utf8")
.split("\n")
.entries()) {
if (!line.trim()) continue
let entry
try {
entry = JSON.parse(line)
} catch (error) {
throw new Error(`${source}:${index + 1}: ${error.message}`)
}
if (
!entry ||
typeof entry.word !== "string" ||
typeof entry.definition !== "string" ||
!entry.word.trim() ||
!entry.definition.trim()
)
throw new Error(
`${source}:${index + 1}: word and definition must be non-empty strings`,
)
const word = entry.word.trim().normalize("NFC")
const definition = entry.definition.trim()
const anchor = word.toLowerCase()
if (anchors.has(anchor))
throw new Error(
`${source}:${index + 1}: duplicate word ${JSON.stringify(word)}`,
)
anchors.add(anchor)
entries.push({ word, definition, anchor })
}
if (!entries.length) throw new Error(`${source}: glossary must not be empty`)
entries.sort((a, b) => (a.word < b.word ? -1 : a.word > b.word ? 1 : 0))
const escape = value =>
value.replace(/[&<>"']/g, char => `&#${char.charCodeAt(0)};`)
const body = [
"<h1>Glossary</h1>",
"<p>Terms used in Tridactyl&#39;s documentation.</p>",
'<dl class="glossary-list">',
...entries.map(
entry =>
`<div class="glossary-entry" id="${escape(entry.anchor)}"><dt><code>${escape(entry.word)}</code></dt><dd>${escape(entry.definition)}</dd></div>`,
),
"</dl>",
].join("\n")
let html = fs.readFileSync(
path.join(root, "src/static/clippy/tutor.template.html"),
"utf8",
)
for (const [marker, replacement] of [
["<title>Tridactyl Tutorial</title>", "<title>Tridactyl Glossary</title>"],
['href="./glossary.html"', 'aria-current="page" href="./glossary.html"'],
["REPLACETHIS", body],
]) {
if (html.split(marker).length !== 2)
throw new Error(
`Expected one ${JSON.stringify(marker)} in tutor.template.html`,
)
html = html.replace(marker, replacement)
}
fs.writeFileSync(
path.join(root, "src/.glossary.generated.json"),
JSON.stringify(entries),
)
fs.writeFileSync(path.join(root, "generated/static/clippy/glossary.html"), html)

View file

@ -13,8 +13,9 @@ dest="../../../generated/static/clippy/"
for page in $pages
do
fileroot=$(echo "$page" | cut -d'.' -f-2)
sed "/REPLACETHIS/,$ d" tutor.template.html > "$dest$fileroot.html"
sed -e "s|href=\"$fileroot.html\"|aria-current=\"page\" &|" \
-e "/REPLACETHIS/,$ d" tutor.template.html > "$dest$fileroot.html"
"$(yarn bin)/marked" "$page" >> "$dest$fileroot.html"
sed "1,/REPLACETHIS/ d" tutor.template.html >> "$dest$fileroot.html"
sed -i.bak "s|\.md|.html|g" "$dest$fileroot.html"
sed -i.bak "s|\(href=['\"]\./[^'\"]*\)\.md\(['\"]\)|\1.html\2|g" "$dest$fileroot.html"
done

7
scripts/minify_json.js Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env node
require("fs").writeFileSync(
process.argv[2],
JSON.stringify(
JSON.parse(require("fs").readFileSync(process.argv[2], "utf8")),
),
)

View file

@ -20,13 +20,14 @@ sed "1,/REPLACETHIS/ d" newtab.template.html >> "$newtabtemp"
sed "/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/,$ d" "$newtabtemp"
# Note: If you're going to change this HTML, make sure you don't break the JS in src/newtab.ts
cat <<EOF
<input type="checkbox" id="spoilerbutton" />
<label for="spoilerbutton" onclick=""><div id="nagbar-changelog">New features!</div>Changelog</label>
<details id="changelog-details">
<summary><span id="nagbar-changelog">New features!</span>Changelog</summary>
<div id="changelog" class="spoiler">
EOF
"$(yarn bin)/marked" ../../CHANGELOG.md
echo """
</div>
</details>
"""
sed "1,/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/ d" "$newtabtemp"
) > "$newtab"

View file

@ -10,6 +10,10 @@ echoe() {
echo "$@" >&2
}
staged() {
git show :"$1"
}
lock() {
local lockfile="$1"
if [ -e "$lockfile" ]; then
@ -27,13 +31,14 @@ unlock() {
trap 'unlock $(git rev-parse --show-toplevel)/.git/index.lock || true' ERR
main() {
local stagedFiles
local stagedFiles originalIndex
stagedFiles="$(cachedTSLintFiles)"$'\n'"$(cachedPrettierFiles)"
if [ -n "$stagedFiles" ]; then
# Could use git-update-index --cacheinfo to add a file without creating directories and stuff.
IFS=$'\n'
for file in $stagedFiles; do
originalIndex=$(git write-tree)
if cmp -s <(staged "$file") "$file"; then
echo "WARN: Staged copy of '$file' matches working copy. Modifying both"
echo "WARN: Modifications may break builds: check that your code still builds"
@ -70,6 +75,8 @@ main() {
rm -rf "$tmpdir"
)
fi
echo "Changes made by pretty to '$file':"
git diff --cached "$originalIndex" -- "$file"
done
fi
}

45
scripts/runwithprofile.js Normal file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env node
const { spawnSync } = require("child_process")
const fs = require("fs")
const os = require("os")
const path = require("path")
const firefoxDir =
process.platform === "win32"
? path.join(process.env.APPDATA || os.homedir(), "Mozilla", "Firefox")
: process.platform === "darwin"
? path.join(os.homedir(), "Library", "Application Support", "Firefox")
: path.join(os.homedir(), ".mozilla", "firefox")
function findDefaultProfile() {
const sections = []
let section
for (const line of fs.readFileSync(path.join(firefoxDir, "profiles.ini"), "utf8").split(/\r?\n/)) {
const header = line.trim().match(/^\[([^\]]+)\]$/)
if (header) {
section = { name: header[1] }
sections.push(section)
} else if (section) {
const separator = line.indexOf("=")
if (separator > 0) section[line.slice(0, separator)] = line.slice(separator + 1)
}
}
const profilePath =
sections.find(section => section.name.startsWith("Profile") && section.Name === "default-release")?.Path ??
sections.find(section => section.name.startsWith("Install") && section.Default)?.Default ??
sections.find(section => section.name.startsWith("Profile") && section.Default === "1")?.Path
if (!profilePath) throw new Error(`No default profile found in ${firefoxDir}`)
return path.isAbsolute(profilePath) ? profilePath : path.resolve(firefoxDir, profilePath)
}
try {
const profile = process.argv[2] || findDefaultProfile()
const command = process.platform === "win32" ? "web-ext.cmd" : "web-ext"
const result = spawnSync(command, ["run", "--source-dir", "build/", "--firefox", "deved", "--pre-install", "--firefox-profile", profile, "--no-reload", "--pref", "browser.startup.page=3", "--pref", "browser.sessionstore.resume_from_crash=true"], { stdio: "inherit" })
if (result.error) throw result.error
process.exitCode = result.status ?? 1
} catch (error) {
console.error(`runwithprofile: ${error.message}`)
process.exitCode = 1
}

View file

@ -4,7 +4,7 @@ set -e
sign_and_submit() {
# Don't trust the return value of web-ext sign.
(source AMOKEYS && (yarn run web-ext sign -s build --api-key "$AMOKEY" --api-secret "$AMOSECRET" "$@" || true))
(set +x; source AMOKEYS && (yarn run web-ext sign -s build --api-key "$AMOKEY" --api-secret "$AMOSECRET" "$@" || true))
}
publish_beta_nonewtab() {
@ -30,6 +30,7 @@ build_no_sign_beta(){
yarn run clean
yarn run build --no-native
scripts/version.js beta
node -e 'const fs = require("fs"), manifest = require("./build/manifest.json"); delete manifest.applications.gecko.update_url; fs.writeFileSync("./build/manifest.json", JSON.stringify(manifest, null, 4))'
sed 's/"name": "Tridactyl"/"name": "Tridactyl: Beta"/' -i build/manifest.json
mkdir -p web-ext-artifacts
yarn run web-ext build --source-dir ./build --overwrite-dest

View file

@ -4,9 +4,10 @@ export LAST_VERSION="$1"
allcontributors="$(git shortlog -sn $LAST_VERSION..HEAD | cut -c8- | awk '!seen[$0]++' | paste -sd "," - | sed 's/,/, /g')"
newcontributors="$(diff --changed-group-format='%<' --unchanged-group-format='' <(git shortlog -sn $LAST_VERSION..HEAD | cut -c8- | awk '!seen[$0]++'| sort) <(git shortlog -sn $LAST_VERSION | cut -c8- | awk '!seen[$0]++'| sort) | paste -sd "," - | sed 's/,/, /g')"
issuereporters="$(gh issue list --state closed --search "closed:>$(git log -1 --format=%cI "$LAST_VERSION")" --limit 10000 --json author --jq '.[] | select(.author) | .author.login' | sort | uniq -c | sort -k1,1nr -k2,2 | awk '{print $2}' | paste -sd "," - | sed 's/,/, /g; s#app/#people who have since deleted their GitHub accounts#g')"
echo "Thanks to all of our contributors for this release: $allcontributors"
echo
echo "Extra special thanks go to $newcontributors"
echo
echo Last, but not least - thank you to everyone who reported issues.
echo "Last, but not least - thank you to everyone who reported the issues we closed in this release: $issuereporters"

471
scripts/typedoc-theme.mjs Normal file
View file

@ -0,0 +1,471 @@
import {
Converter,
DefaultTheme,
IntrinsicType,
JSX,
KindRouter,
PageKind,
Reflection,
ReflectionKind,
ReflectionType,
Slugger,
TypeScript as ts,
} from "typedoc"
const h = JSX.createElement
const css = href => h("link", { rel: "stylesheet", href })
const cleanName = name => name.replace(/[^a-z0-9]/gi, "_").toLowerCase()
const isGeneratedSource = source =>
/\/\.[^/]+\.generated\.ts$/.test(source.fullFileName)
function rewriteWikiLinks(parts, owner, reflections) {
return parts.flatMap(part => {
if (part.kind !== "text" || !part.text.includes("[[")) return part
const out = []
let end = 0
for (const match of part.text.matchAll(
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
)) {
if (match.index > end)
out.push({
kind: "text",
text: part.text.slice(end, match.index),
})
const candidates = reflections.get(match[1]) || []
const ownerModule = getModule(owner)
const target =
(!owner.kindOf(
ReflectionKind.Module | ReflectionKind.Namespace,
) &&
candidates.find(
candidate => candidate.parent === owner.parent,
)) ||
candidates.find(
candidate => getModule(candidate) === ownerModule,
) ||
candidates.find(
candidate =>
!candidate.kindOf(
ReflectionKind.Module | ReflectionKind.Namespace,
) && !candidate.sources?.some(isGeneratedSource),
) ||
candidates.find(
candidate =>
!candidate.kindOf(
ReflectionKind.Module | ReflectionKind.Namespace,
),
) ||
candidates[0]
out.push(
target
? {
kind: "inline-tag",
tag: "@link",
text: match[2] || match[1],
target,
}
: { kind: "text", text: match[2] || match[1] },
)
end = match.index + match[0].length
}
if (end < part.text.length)
out.push({ kind: "text", text: part.text.slice(end) })
return out
})
}
function getModule(reflection) {
while (reflection.parent && !reflection.parent.isProject())
reflection = reflection.parent
return reflection
}
function escapeDependencyTags(parts) {
return parts.map(part =>
part.kind === "text"
? {
...part,
text: part.text.replace(
/<(\/?)(iframe|object|frame)>/gi,
"&lt;$1$2&gt;",
),
}
: part,
)
}
function restoreWikiLinks(context) {
const reflections = new Map()
for (const reflection of Object.values(context.project.reflections)) {
if (!reflection.isDeclaration()) continue
const matches = reflections.get(reflection.name) || []
matches.push(reflection)
reflections.set(reflection.name, matches)
}
for (const reflection of Object.values(context.project.reflections)) {
const fromDependency = reflection.sources?.some(source =>
source.fullFileName.includes("/node_modules/"),
)
for (const source of reflection.sources || [])
if (
source.fullFileName.includes("/node_modules/") ||
isGeneratedSource(source)
)
source.url = undefined
const comment = reflection.comment
if (!comment) continue
comment.summary = rewriteWikiLinks(
comment.summary,
reflection,
reflections,
)
for (const tag of comment.blockTags)
tag.content = rewriteWikiLinks(tag.content, reflection, reflections)
if (fromDependency) {
comment.summary = escapeDependencyTags(comment.summary)
for (const tag of comment.blockTags)
tag.content = escapeDependencyTags(tag.content)
}
}
if (context.project.readme)
context.project.readme = rewriteWikiLinks(
context.project.readme,
context.project,
reflections,
)
}
function restoreObjectLiteral(context, reflection) {
const node = context.getSymbolFromReflection(reflection)?.valueDeclaration
const initializer = node && "initializer" in node && node.initializer
if (!initializer || !ts.isObjectLiteralExpression(initializer)) return
if (!initializer.properties.length) return
if (reflection.type instanceof ReflectionType)
context.project.removeTypeReflections(reflection.type)
const inferred = context.converter.convertType(
context.withScope(reflection),
context.checker.getTypeAtLocation(initializer),
)
if (!(inferred instanceof ReflectionType)) return
reflection.type = new IntrinsicType("object")
context.project.mergeReflections(inferred.declaration, reflection)
}
function restoreCommandlineFns(context) {
const commandline = context.project.getChildByName("commandline_frame")
const functions = commandline?.getChildByName("commandlineFns")
if (functions) context.project.mergeReflections(functions, commandline)
}
class LegacySlugger extends Slugger {
serialize(value) {
return value.startsWith("\0") ? value.slice(1) : super.serialize(value)
}
}
class TridactylRouter extends KindRouter {
aliases = new Map()
aliasCounts = new Map()
getSlugger(target) {
while (!this.sluggers.has(target)) target = target.parent
let slugger = this.sluggers.get(target)
if (!(slugger instanceof LegacySlugger)) {
slugger = new LegacySlugger(this.sluggerConfiguration)
this.sluggers.set(target, slugger)
}
return slugger
}
getPageKind(target) {
if (!(target instanceof Reflection)) return
if (
target.kindOf(
ReflectionKind.Class |
ReflectionKind.Interface |
ReflectionKind.Enum |
ReflectionKind.Module |
ReflectionKind.Namespace,
)
)
return PageKind.Reflection
if (target.kindOf(ReflectionKind.Document)) return PageKind.Document
}
getIdealBaseName(reflection) {
const directory = this.directories.get(reflection.kind)
const parts = []
do {
let name = reflection.name
if (
reflection.parent?.isProject() &&
reflection.kind === ReflectionKind.Module
)
name = `"src/${name}"`
parts.unshift(cleanName(name))
reflection = reflection.parent
} while (reflection && !reflection.isProject())
return `${directory}/${parts.join(".")}`
}
getLegacyAlias(reflection, pageTarget) {
if (this.aliases.has(reflection)) return this.aliases.get(reflection)
const base = cleanName(reflection.name) || `reflection-${reflection.id}`
let counts = this.aliasCounts.get(pageTarget)
if (!counts) this.aliasCounts.set(pageTarget, (counts = new Map()))
const count = counts.get(base) || 0
const alias = count ? `${base}-${count}` : base
counts.set(base, count + 1)
this.aliases.set(reflection, alias)
return alias
}
createAnchor(target, pageTarget) {
if (!target.isDeclaration())
return super.createAnchor(target, pageTarget)
const parts = []
for (
let current = target;
current !== pageTarget;
current = current.parent
)
parts.unshift(this.getLegacyAlias(current, pageTarget))
return this.getSlugger(pageTarget).slug("\0" + parts.join("."))
}
buildPages(project) {
this.aliases.clear()
this.aliasCounts.clear()
const pages = super.buildPages(project)
const globals = pages.find(page => page.url === "modules.html")
if (globals) {
globals.url = "globals.html"
this.fullUrls.set(project, "globals.html")
}
return pages
}
}
function parseFlagTag(tag) {
const parts = tag.content || []
let text = ""
let trailing = []
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
const match = part.kind === "text" && /\r?\n[ \t]*\r?\n/.exec(part.text)
if (!match) {
text += part.text || ""
continue
}
text += part.text.slice(0, match.index)
trailing = [
{
...part,
text: part.text.slice(match.index + match[0].length),
},
...parts.slice(i + 1),
]
break
}
const m = /^(-\S+)[ \t]+([^\n]+)\n*([\s\S]*)$/.exec(text.trim())
if (!m) return undefined
const [, flag, short, rest] = m
const elaboration = rest.trim()
return [flag, short, elaboration, trailing]
}
function renderFlagList(context, parsed) {
if (parsed.length === 0) return null
return h(
"ul",
{ class: "tsd-tag-flag tsd-parameter-list" },
parsed.map(([flag, short, elaboration]) =>
h(
"li",
null,
h("code", null, flag),
" ",
short,
elaboration &&
context.displayParts([{ kind: "text", text: elaboration }]),
),
),
)
}
class TridactylTheme extends DefaultTheme {
getRenderContext(page) {
const context = super.getRenderContext(page)
const defaultCommentSummary = context.commentSummary
context.commentSummary = props => {
const blockTags = props.comment?.blockTags || []
if (!blockTags.some(tag => tag.tag === "@flag"))
return defaultCommentSummary(props)
const summaryHeadingCount = page.pageHeadings.length
const nodes = [context.displayParts(props.comment?.summary || [])]
page.pageHeadings.length = summaryHeadingCount
let flagRun = []
const flushFlags = () => {
if (flagRun.length === 0) return
nodes.push(renderFlagList(context, flagRun))
flagRun = []
}
for (const tag of blockTags) {
if (tag.tag === "@flag") {
tag.skipRendering = true
const parsed = parseFlagTag(tag)
if (!parsed) continue
const [flag, short, elaboration, trailing] = parsed
flagRun.push([flag, short, elaboration])
if (trailing.length === 0) continue
flushFlags()
const headingCount = page.pageHeadings.length
nodes.push(context.displayParts(trailing))
page.pageHeadings.length = headingCount
}
}
flushFlags()
return h(JSX.Fragment, null, ...nodes)
}
return context
}
getReflectionClasses(reflection) {
const kind = ReflectionKind.classString(reflection.kind)
const parent =
reflection.parent &&
ReflectionKind.classString(reflection.parent.kind).replace(
"tsd-kind-",
"tsd-parent-kind-",
)
return [super.getReflectionClasses(reflection), kind, parent]
.filter(Boolean)
.join(" ")
}
constructor(renderer) {
super(renderer)
const reflectionTemplate = this.reflectionTemplate
this.reflectionTemplate = page => {
const content = reflectionTemplate(page)
if (
!page.model.kindOf(
ReflectionKind.Module | ReflectionKind.Namespace,
)
)
return content
const context = this.getRenderContext(page)
page.pageSections.length = 0
const renderMember = context.member
context.member = reflection =>
reflection.isReference() ? null : renderMember(reflection)
return h(JSX.Fragment, null, content, context.members(page.model))
}
this.defaultLayoutTemplate = (page, template) => {
const context = this.getRenderContext(page)
const content = template(page)
const helpLink = (label, href) =>
h("li", null, h("a", { href }, label))
const docLink = (label, url) =>
helpLink(label, context.relativeURL(url))
return h(
"html",
{ class: "minimal no-js TridactylOwnNamespace" },
h(
"head",
null,
h("meta", { charset: "utf-8" }),
h("meta", {
"http-equiv": "X-UA-Compatible",
content: "IE=edge",
}),
h(
"title",
null,
`${page.model.name} | ${page.project.name}`,
),
h("meta", {
name: "viewport",
content: "width=device-width, initial-scale=1",
}),
css(context.relativeURL("assets/style.css")),
css(context.relativeURL("assets/highlight.css")),
h("script", { src: "/content.js" }),
h("script", { src: "/help.js" }),
css("/static/css/content.css"),
css("/static/css/hint.css"),
css("/static/css/viewsource.css"),
css("/static/typedoc/assets/css/main.css"),
),
h(
"body",
null,
h(
"nav",
{ class: "tsd-navigation secondary scroller" },
h(
"section",
{ class: "TridactylHelpNavigation" },
h("h3", null, "Help Pages"),
h(
"ul",
null,
docLink(
"Commands",
"modules/_src_excmds_.html",
),
docLink(
"Settings",
"classes/_src_lib_config_.default_config.html",
),
docLink("Tutorial", "../clippy/1-tutor.html"),
docLink(
"Editor Functions",
"modules/_src_lib_editor_.html",
),
docLink(
"Command-Line Functions",
"modules/_src_commandline_frame_.html",
),
docLink(
"Hint Mode Commands",
"modules/_src_content_hinting_.html",
),
helpLink(
"Wiki",
"https://github.com/tridactyl/tridactyl/wiki",
),
),
),
context.pageNavigation(page),
),
h(
"div",
{ class: "container container-main scroller" },
h("div", { class: "content-wrap" }, content),
),
),
)
}
}
}
export function load(app) {
app.converter.on(Converter.EVENT_RESOLVE_BEGIN, restoreCommandlineFns)
app.converter.on(Converter.EVENT_RESOLVE_END, restoreWikiLinks, 100)
app.converter.on(
Converter.EVENT_CREATE_DECLARATION,
restoreObjectLiteral,
100,
)
app.renderer.defineRouter("tridactyl", TridactylRouter)
app.renderer.defineTheme("tridactyl", TridactylTheme)
}

View file

@ -1,6 +1,6 @@
#!/usr/bin/env node
const { exec } = require("child_process")
const { exec, execFileSync } = require("child_process")
const fs = require("fs")
function bump_version(versionstr, component = 2) {
@ -12,7 +12,33 @@ function bump_version(versionstr, component = 2) {
return versionarr.join(".")
}
async function add_beta(versionstr) {
function release_name(manifest) {
return (manifest.version_name || manifest.version)
.slice(manifest.version.length)
.trim()
}
function validate_release_version(manifest) {
if (!/^\d+\.\d+\.\d+$/.test(manifest.version) || manifest.version_name !== [manifest.version, release_name(manifest)].filter(Boolean).join(" ")) {
throw new Error("Manifest version_name must be the version followed by an optional release name")
}
}
function tagged(version) {
return fs.existsSync(".git") && execFileSync("git", ["tag", "--list", version]).toString().trim() !== ""
}
function set_release_version(manifest, component, name = "") {
if (![0, 1, 2].includes(component)) throw new Error("Version component must be 0, 1 or 2")
if (component === 2 && name) throw new Error("Only major and minor releases can be named")
const nameForRelease =
component === 2 ? release_name(manifest) : name.trim()
manifest.version = bump_version(manifest.version, component)
manifest.version_name = [manifest.version, nameForRelease].filter(Boolean).join(" ")
}
async function beta_number() {
await fs.promises.mkdir(".build_cache", {recursive: true})
try {
await fs.promises.access(".git")
@ -26,7 +52,7 @@ async function add_beta(versionstr) {
catch {
; // Not in a git directory - don't do anything
}
return versionstr + "pre" + (await fs.promises.readFile(".build_cache/count", {encoding: "utf8"})).trim()
return (await fs.promises.readFile(".build_cache/count", {encoding: "utf8"})).trim()
}
async function get_hash() {
@ -81,34 +107,76 @@ function save_manifest(filename, manifest) {
fs.writeFileSync(filename, JSON.stringify(manifest, null, 4))
}
function set_beta_version(manifest, number, hash) {
const version = manifest.version
const name = release_name(manifest)
manifest.version = `${version}.${number}`
manifest.version_name = [`${version}pre${number}-${hash}`, name].filter(Boolean).join(" ")
}
async function main() {
let filename, manifest
switch (process.argv[2]) {
case "bump":
// Load src manifest and bump
case "next": {
filename = "./src/manifest.json"
manifest = require("." + filename)
manifest.version = bump_version(
manifest.version,
if (!tagged(manifest.version)) throw new Error(`Version ${manifest.version} is not tagged`)
set_release_version(
manifest,
Number(process.argv[3]),
process.argv.slice(4).join(" "),
)
manifest.version_name = manifest.version
validate_release_version(manifest)
if (tagged(manifest.version)) throw new Error(`Version ${manifest.version} is already tagged`)
save_manifest(filename, manifest)
exec(
`git add ${filename} && git commit -m 'release ${
manifest.version
}' && git tag ${manifest.version}`,
break
}
case "release": {
filename = "./src/manifest.json"
manifest = require("." + filename)
validate_release_version(manifest)
if (tagged(manifest.version)) throw new Error(`Version ${manifest.version} is already tagged`)
if (execFileSync("git", ["status", "--porcelain"]).toString().trim()) throw new Error("Release requires a clean worktree")
const changelog = fs.readFileSync("./CHANGELOG.md", "utf8")
const releaseHeading = `Release ${manifest.version} / Unreleased`
const releaseNotes = changelog
.split(/(?=^#+ Release )/m)
.find(notes =>
notes.split("\n", 1)[0].trimEnd().endsWith(releaseHeading),
)
if (!releaseNotes) {
throw new Error(`No ${releaseHeading} changelog entry`)
}
const datedReleaseNotes = releaseNotes.replace(
"Unreleased",
new Date().toISOString().slice(0, 10),
)
fs.writeFileSync(
"./CHANGELOG.md",
changelog.replace(releaseNotes, () => datedReleaseNotes),
)
execFileSync("git", ["add", "./CHANGELOG.md"])
execFileSync("git", [
"commit",
"--cleanup=verbatim",
"-m",
`release ${manifest.version}`,
"-m",
datedReleaseNotes.trim(),
])
execFileSync("git", ["tag", manifest.version])
console.log(
`Make sure you use the release checklist before committing this.`,
)
console.log(`https://github.com/tridactyl/tridactyl/issues/714`)
break
}
case "beta":
filename = "./build/manifest.json"
manifest = require("." + filename)
manifest.version = await add_beta(manifest.version)
manifest.version_name = manifest.version + "-" + (await get_hash())
validate_release_version(manifest)
if (tagged(manifest.version)) throw new Error(`Version ${manifest.version} is already tagged`)
set_beta_version(manifest, await beta_number(), await get_hash())
manifest.applications.gecko.update_url =
"https://tridactyl.cmcaine.co.uk/betas/updates.json"
@ -130,4 +198,6 @@ async function main() {
}
}
main()
if (require.main === module) main()
module.exports = { set_beta_version, set_release_version }

22
scripts/version.test.js Normal file
View file

@ -0,0 +1,22 @@
const { set_beta_version, set_release_version } = require("./version")
test("manages major and minor release names", () => {
const manifest = { version: "1.24.6", version_name: "1.24.6" }
set_release_version(manifest, 1, "Carpenter")
expect(manifest.version_name).toBe("1.25.0 Carpenter")
set_release_version(manifest, 2)
expect(manifest.version_name).toBe("1.25.1 Carpenter")
set_release_version(manifest, 0, "Joiner")
expect(manifest.version_name).toBe("2.0.0 Joiner")
expect(() =>
set_release_version({ version: "1.25.0" }, 2, "Carpenter"),
).toThrow()
set_beta_version(manifest, "7685", "1ac287cf")
expect(manifest.version).toBe("2.0.0.7685")
expect(manifest.version_name).toBe("2.0.0pre7685-1ac287cf Joiner")
})

View file

@ -20,21 +20,25 @@ export WINEDEBUG="fixme-all"
# stop wine whining
export DISPLAY=
PREREQUISITES="tput printf 7z wine"
PREREQUISITES="tput printf sort 7z wine"
MIN_WINE_VER="4"
MIN_7ZIP_VER="16"
versionAtLeast() {
printf '%s\n' "$1" "$2" | sort -VC
}
checkRequiredVersions() {
if ! 7z | awk '/Version/{print $3}' | grep -q "${MIN_7ZIP_VER}"; then
if ! versionAtLeast "${MIN_7ZIP_VER}" "$(7z | awk '/Version/{print $3}')"; then
colorEcho \
'[-] p7zip minimum version '"${MIN_7ZIP_VER}"' required\n' \
"alert"
exit 1
fi
if ! wine --version 2> /dev/null | grep -q "wine-${MIN_WINE_VER}"; then
colorecho \
if ! versionAtLeast "${MIN_WINE_VER}" "$(wine --version 2> /dev/null | cut -d- -f2-)"; then
colorEcho \
'[-] wine minimum version '"${MIN_WINE_VER}"' required\n' \
"alert"
exit 1

View file

@ -8,6 +8,7 @@ import { omniscient_controller } from "@src/lib/omniscient_controller"
import * as perf from "@src/perf"
import { listenForCounters } from "@src/perf"
import * as messaging from "@src/lib/messaging"
import { messageTabChanges } from "@src/background/tab_changes"
import * as excmds_background from "@src/.excmds_background.generated"
import { CmdlineCmds } from "@src/background/commandline_cmds"
import { EditorCmds } from "@src/background/editor"
@ -27,6 +28,7 @@ import * as omnibox from "@src/background/omnibox"
import * as R from "ramda"
import * as webrequests from "@src/background/webrequests"
import * as commands from "@src/background/commands"
import * as browser_action from "@src/background/browser_action"
import * as meta from "@src/background/meta"
import * as Logging from "@src/lib/logging"
import * as Proxy from "@src/lib/proxy"
@ -48,7 +50,10 @@ import { tabsProxy } from "@src/lib/tabs"
state,
webext,
webrequests,
l: (prom: Promise<any>) => prom.then(console.log).catch(console.error),
l: (value: any) =>
typeof value?.then === "function"
? value.then(console.log).catch(console.error)
: console.log(value),
contentLocation: window.location,
R,
perf,
@ -69,22 +74,24 @@ controller.setExCmds({
})
// {{{ tri.contentLocation
// When loading the background, use the active tab to know what the current content url is
browser.tabs.query({ currentWindow: true, active: true }).then(t => {
;(window as any).tri.contentLocation = new URL(t[0].url)
})
// After that, on every tab change, update the current url
let contentLocationCount = 0
browser.tabs.onActivated.addListener(ev => {
function updateContentLocation(windowId = browser.windows.WINDOW_ID_CURRENT) {
const myId = contentLocationCount + 1
contentLocationCount = myId
browser.tabs.get(ev.tabId).then(t => {
// Note: we're using contentLocationCount and myId in order to make sure that only the last onActivated event is used in order to set contentLocation
// This is needed because otherWise the following chain of execution might happen: onActivated1 => onActivated2 => tabs.get2 => tabs.get1
if (contentLocationCount === myId) {
;(window as any).tri.contentLocation = new URL(t.url)
}
})
browser.tabs
.query({ windowId, active: true })
.then(t => {
// Ignore stale queries when focus or active tabs change quickly.
if (contentLocationCount === myId && t[0]?.url) {
;(window as any).tri.contentLocation = new URL(t[0].url)
}
})
.catch(() => undefined)
}
browser.tabs.onActivated.addListener(() => updateContentLocation())
browser.windows.onFocusChanged.addListener(windowId => {
if (windowId === browser.windows.WINDOW_ID_NONE) return
updateContentLocation(windowId)
})
browser.proxy.onRequest.addListener(Proxy.onRequestListener, {
@ -94,32 +101,44 @@ browser.proxy.onRequest.addListener(Proxy.onRequestListener, {
/**
* Declare Tab Event Listeners
*/
browser.tabs.onRemoved.addListener(tabId => {
messaging.messageAllTabs("tab_changes", "tab_close", [tabId])
})
const tabChangeListener = (command: string) => () => messageTabChanges(command)
browser.tabs.onRemoved.addListener(tabChangeListener("tab_close"))
// Fired when a tab is attached to a window, for example because it was moved between windows.
browser.tabs.onAttached.addListener(tabId => {
messaging.messageAllTabs("tab_changes", "tab_attached", [tabId])
})
browser.tabs.onAttached.addListener(tabChangeListener("tab_attached"))
// Fired when a tab is created. Note that the tab's URL may not be set at the time this event fired.
browser.tabs.onCreated.addListener(tabId => {
messaging.messageAllTabs("tab_changes", "tab_created", [tabId])
})
browser.tabs.onCreated.addListener(tabChangeListener("tab_created"))
// Fired when a tab is detached from a window, for example because it is being moved between windows.
browser.tabs.onDetached.addListener(tabId => {
messaging.messageAllTabs("tab_changes", "tab_detached", [tabId])
})
browser.tabs.onDetached.addListener(tabChangeListener("tab_detached"))
// Fired when a tab is moved within a window.
browser.tabs.onMoved.addListener(tabId => {
messaging.messageAllTabs("tab_changes", "tab_moved", [tabId])
})
browser.tabs.onMoved.addListener(tabChangeListener("tab_moved"))
browser.tabs.onUpdated.addListener(
tabChangeListener("tab_updated"),
{
properties: [
"audible",
"discarded",
"favIconUrl",
"hidden",
"mutedInfo",
"pinned",
"title",
"url",
],
},
)
browser.tabs.onActivated.addListener(tabChangeListener("tab_activated"))
// Update on navigation too (but remember that sometimes people open tabs in the background :) )
browser.webNavigation.onDOMContentLoaded.addListener(() => {
browser.tabs.query({ currentWindow: true, active: true }).then(t => {
;(window as any).tri.contentLocation = new URL(t[0].url)
})
updateContentLocation()
})
const messageHistoryState = (details: { frameId: number; tabId: number }) => {
if (details.frameId !== 0) return
messaging.messageTab(details.tabId, "history_state").catch(() => undefined)
}
browser.webNavigation.onHistoryStateUpdated.addListener(messageHistoryState)
browser.webNavigation.onReferenceFragmentUpdated.addListener(messageHistoryState)
updateContentLocation()
// Prevent Tridactyl from being updated while it is running in the hope of fixing #290
browser.runtime.onUpdateAvailable.addListener(_ => undefined)
@ -195,27 +214,6 @@ for (const requestEvent of webrequests.requestEvents) {
})
}
config.addChangeListener("autocmds", (previous, current) =>
webrequests.requestEvents.forEach(
requestEvent =>
// If there are autocmd(s) for this requestEvent
current[requestEvent] !== undefined &&
Object.entries(
current[requestEvent] as Record<string, string>,
).forEach(([pattern, func]) => {
// R.path returns undefined if any part of the path is missing rather than saying "computer says no"
const path = R.path([requestEvent, pattern])
// If this is a new autocmd, register it
path(current) !== path(previous) &&
webrequests.registerWebRequestAutocmd(
requestEvent,
pattern,
func,
)
}),
),
)
// }}}
@ -244,6 +242,7 @@ browser.webRequest.onBeforeRequest.addListener(
)
browser.tabs.onCreated.addListener(aucon.tabCreatedListener)
browser.tabs.onRemoved.addListener(aucon.tabRemovedListener)
// }}}
@ -252,6 +251,18 @@ browser.tabs.onCreated.addListener(aucon.tabCreatedListener)
// An object to collect all of our statistics in one place.
const statsLogger: perf.StatsLogger = new perf.StatsLogger()
const messages = {
browser_action_background: {
getState: browser_action.getState,
toggle: browser_action.toggle,
},
config_background: {
clear: config.clear,
pull: config.pull,
push: config.push,
ready: () => config.getAsync().then(() => undefined),
set: config.set,
unset: config.unset,
},
excmd_background: excmds_background,
controller_background: controller,
performance_background: statsLogger,
@ -287,9 +298,8 @@ omnibox.init()
// }}}
setTimeout(config.update, 5000)
commands.updateListener()
browser_action.init()
// {{{ Obey Mozilla's orders https://github.com/tridactyl/tridactyl/issues/1800

View file

@ -0,0 +1,43 @@
import * as config from "@src/lib/config"
import { getState, init, toggle } from "@src/background/browser_action"
jest.mock("@src/lib/config", () => {
const userconfig = { superignore: "false" }
return {
DEFAULTS: { superignore: "false" },
USERCONFIG: userconfig,
getAsync: jest.fn().mockResolvedValue(undefined),
set: jest.fn((_key, value) => {
userconfig.superignore = value
return Promise.resolve()
}),
addChangeListener: jest.fn(),
}
})
test("browser action toggles superignore without reloading tabs", async () => {
await config.set("superignore", "false")
init()
await expect(getState()).resolves.toBe("false")
await expect(Promise.all([toggle(), toggle()])).resolves.toEqual([
"true",
"false",
])
expect(config.USERCONFIG.superignore).toBe("false")
expect(browser.browserAction.setBadgeText).toHaveBeenCalledWith({
text: "OFF",
})
expect(browser.tabs.reload).not.toHaveBeenCalled()
expect(browser.browserAction.onClicked.addListener).not.toHaveBeenCalled()
expect(browser.browserAction.setTitle).toHaveBeenLastCalledWith({
title: "Tridactyl enabled",
})
jest.mocked(config.set).mockImplementationOnce(async (_key, value) => {
config.USERCONFIG.superignore = value as "true" | "false"
throw new Error("write failed")
})
await expect(toggle()).rejects.toThrow("write failed")
expect(config.USERCONFIG.superignore).toBe("false")
})

View file

@ -0,0 +1,48 @@
import * as config from "@src/lib/config"
const ready = config.getAsync()
const superignore = () =>
config.USERCONFIG.superignore ?? config.DEFAULTS.superignore
function updateButton(value) {
const disabled = value === "true"
return Promise.all([
browser.browserAction.setBadgeText({ text: disabled ? "OFF" : "" }),
browser.browserAction.setTitle({
title: disabled ? "Tridactyl disabled" : "Tridactyl enabled",
}),
])
}
export async function getState() {
await ready
return superignore()
}
let toggleQueue: Promise<unknown> = Promise.resolve()
export function toggle() {
const pending = toggleQueue
.catch(() => undefined)
.then(async () => {
const previous = config.USERCONFIG.superignore
const value = (await getState()) === "true" ? "false" : "true"
try {
await config.set("superignore", value)
} catch (error) {
if (previous === undefined) delete config.USERCONFIG.superignore
else config.USERCONFIG.superignore = previous
throw error
}
updateButton(value).catch(console.error)
return value
})
toggleQueue = pending
return pending
}
export function init() {
config.addChangeListener("superignore", (_, value) =>
updateButton(value).catch(console.error),
)
getState().then(updateButton).catch(console.error)
}

View file

@ -0,0 +1,38 @@
import * as controller from "@src/lib/controller"
import * as config from "@src/lib/config"
import { rcFileToExCmds, runRc } from "@src/background/config_rc"
jest.mock("@src/lib/controller")
global.structuredClone ??= value => JSON.parse(JSON.stringify(value))
const backslash = "\\"
test.each([
[`set foo one ${backslash}\ntwo`, ["set foo one two"]],
[
`keymap foo ${backslash}${backslash}\nset bar baz`,
[`keymap foo ${backslash}`, "set bar baz"],
],
[`keymap foo ${backslash}${backslash}\n`, [`keymap foo ${backslash}`]],
[`keymap foo ${backslash}`, [`keymap foo ${backslash}`]],
])("parses RC line ending backslashes", (rc, expected) => {
expect(rcFileToExCmds(rc)).toEqual(expected)
})
test("runRc updates and saves versioned config", async () => {
await config.clear()
jest.mocked(controller.acceptExCmd).mockImplementation(async cmd => {
const [, key, value] = cmd.split(" ")
await config.set(key, value)
})
await runRc("set configversion 1.0\nset vimium-gi false")
expect(browser.storage.local.set).toHaveBeenLastCalledWith(
expect.objectContaining({
userconfig: expect.objectContaining({
configversion: "2.0",
gimode: "firefox",
}),
}),
)
})

View file

@ -1,4 +1,5 @@
import * as controller from "@src/lib/controller"
import * as config from "@src/lib/config"
import * as Native from "@src/lib/native"
export async function source(filename = "auto") {
@ -48,14 +49,16 @@ export async function writeRc(conf: string, force = false, filename = "auto") {
} else {
path = filename
}
return await Native.writerc(path, force, conf)
await Native.writerc(path, force, conf)
return path
}
export async function runRc(rc: string) {
for (const cmd of rcFileToExCmds(rc)) {
await new Promise(resolve => setTimeout(resolve, 100))
await controller.acceptExCmd(cmd)
}
// Sourced commands have already been saved to the current local config.
await config.update(true)
}
export function rcFileToExCmds(rcText: string): string[] {
@ -69,10 +72,13 @@ export function rcFileToExCmds(rcText: string): string[] {
!x.trim().startsWith('"') &&
!x.trim().startsWith("#"),
)
const res = ex.join("\n")
const res = ex.join("\n") + (rcText.endsWith("\n") ? "\n" : "")
// string-join lines that end with /
const joined = res.replace(/\\\n/g, "")
// Join lines ending in an unescaped backslash and unescape trailing pairs.
const joined = res.replace(/(\\+)\n/g, (_, backslashes: string) => {
const escaped = "\\".repeat(Math.floor(backslashes.length / 2))
return escaped + (backslashes.length % 2 === 0 ? "\n" : "")
})
return joined.split("\n")
return joined.replace(/\n$/, "").split("\n")
}

View file

@ -79,10 +79,10 @@ export async function downloadUrl(url: string, saveAs: boolean) {
*
* Note: this requires a native messenger >=0.1.9. Make sure to nativegate for this.
*
* @param URL the URL to download
* @param url the URL to download
* @param saveAs If beginning with a slash, this is the absolute path the document should be moved to. If the first character of the string is a tilda, it will be expanded to an absolute path to the user's home directory. If saveAs begins with any other character, it will be considered a path relative to where the native messenger binary is located (e.g. "$HOME/.local/share/tridactyl" on linux).
* @param If true, overwrite the destination file, returns error code 1 otherwise if file exists
* @param If true, cleans up temporary downloaded source file e.g. in $HOME/Downlods/downloaded.doc when the move operation fails e.g. due to target destination exists, OS error etc.
* @param overwrite If true, overwrite the destination file, returns error code 1 otherwise if file exists
* @param cleanup If true, cleans up temporary downloaded source file e.g. in $HOME/Downlods/downloaded.doc when the move operation fails e.g. due to target destination exists, OS error etc.
*/
export async function downloadUrlAs(
url: string,
@ -137,12 +137,18 @@ export async function downloadUrlAs(
})
)[0]
if (downloadDelta.state.current === "complete") {
const operation = await Native.move(
const placeholder = config.get("downloadfilenamemarker")
let finalSaveAs = saveAs
if (placeholder.length > 0 && finalSaveAs.includes(placeholder)) {
finalSaveAs = finalSaveAs.split(placeholder).join(fileName)
}
const operation = await Native.move(
downloadItem.filename,
saveAs,
finalSaveAs,
overwrite,
cleanup,
)
)
const code2human = n =>
R.defaultTo(
"Unknown error",
@ -161,7 +167,7 @@ export async function downloadUrlAs(
),
)
} else {
resolve(downloadItem.filename)
resolve({filename: downloadItem.filename, finalSaveAs})
}
} else {
reject(

View file

@ -0,0 +1,29 @@
import { messageTabChanges } from "@src/background/tab_changes"
test("tab change bursts are delivered in order without overlap", async () => {
jest.useFakeTimers()
let finishSending
const query = jest.mocked(browser.tabs.query)
const send = jest.mocked(browser.tabs.sendMessage)
query.mockResolvedValue([{ id: 1 }, { id: 2 }] as browser.tabs.Tab[])
const firstSend = new Promise<void>(resolve => (finishSending = resolve))
send.mockReturnValueOnce(firstSend).mockResolvedValue(undefined)
const waitForSends = async (count: number) => {
for (let i = 0; i < 10 && send.mock.calls.length < count; i++)
await Promise.resolve()
}
for (let id = 0; id < 1500; id++) messageTabChanges("tab_created")
jest.runOnlyPendingTimers()
await waitForSends(2)
expect(query).toHaveBeenCalledWith({ active: true })
expect(send.mock.calls.slice(0, 2)).toEqual(
[1, 2].map(id => [id, { type: "tab_changes", command: "priority" }]),
)
messageTabChanges("tab_updated")
expect(send).toHaveBeenCalledTimes(2)
finishSending()
await waitForSends(4)
expect(send.mock.calls.slice(2)).toEqual(
[1, 2].map(id => [id, { type: "tab_changes", command: "updated" }]),
)
jest.useRealTimers()
})

View file

@ -0,0 +1,35 @@
const UPDATE_PENDING = 1
const PRIORITY_PENDING = 2
let tabChanges = 0
let sendingTabChanges: Promise<void>
export function messageTabChanges(command: string) {
tabChanges |=
/^tab_(?:close|created|moved|activated|attached|detached)$/u.test(
command,
)
? PRIORITY_PENDING
: UPDATE_PENDING
sendingTabChanges ||= new Promise<void>(resolve =>
setTimeout(resolve, 0),
).then(async () => {
while (tabChanges) {
const changes = tabChanges
tabChanges = 0
const priority = changes & PRIORITY_PENDING
const tabs = await browser.tabs
.query({ active: true })
.catch(() => [])
await Promise.all(
tabs.map(tab =>
browser.tabs
.sendMessage(tab.id, {
type: "tab_changes",
command: priority ? "priority" : "updated",
})
.catch(() => undefined),
),
)
}
sendingTabChanges = undefined
})
}

View file

@ -12,27 +12,41 @@ export const requestEventExpraInfoSpecMap = {
export const requestEvents = Object.keys(requestEventExpraInfoSpecMap)
// I'm being lazy - strictly the functions map strings to void | blocking responses
// eslint-disable-next-line @typescript-eslint/ban-types
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export const LISTENERS: Record<string, Record<string, Function>> = {}
export const registerWebRequestAutocmd = (
export const registerWebRequestAutocmd = async (
requestEvent: string,
pattern: string,
func: string,
) => {
// I'm being lazy - strictly the functions map strings to void | blocking responses
// eslint-disable-next-line @typescript-eslint/ban-types
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const listener = eval(func) as Function
if (!LISTENERS[requestEvent]) LISTENERS[requestEvent] = {}
LISTENERS[requestEvent][pattern] = listener
return browser.webRequest["on" + requestEvent].addListener(
await browser.webRequest["on" + requestEvent].addListener(
listener,
{ urls: [pattern] },
requestEventExpraInfoSpecMap[requestEvent],
)
const oldListener = LISTENERS[requestEvent][pattern];
// Add the new listener to our list if everything was successful
LISTENERS[requestEvent][pattern] = listener
// Remove any previously registered autocmd for the same pattern
if (oldListener) {
await browser.webRequest["on" + requestEvent].removeListener(
oldListener
)
}
}
export const unregisterWebRequestAutocmd = (requestEvent, pattern) =>
browser.webRequest["on" + requestEvent].removeListener(
LISTENERS[requestEvent][pattern],
)
export const unregisterWebRequestAutocmd = async (requestEvent, pattern) => {
if (LISTENERS[requestEvent] && LISTENERS[requestEvent][pattern]) {
await browser.webRequest["on" + requestEvent].removeListener(
LISTENERS[requestEvent][pattern],
)
}
}

View file

@ -0,0 +1,55 @@
jest.mocked(browser.runtime.sendMessage)
.mockResolvedValueOnce("false")
.mockResolvedValueOnce("true")
const flush = () => new Promise(resolve => setTimeout(resolve))
test("popup separates toggling from reloading the active tab", async () => {
document.body.innerHTML = `
<p id="state"></p>
<button id="toggle" disabled></button>
<button id="reload" disabled></button>`
jest.mocked(browser.tabs.query).mockResolvedValue([
{ id: 7 } as browser.tabs.Tab,
])
jest.mocked(browser.tabs.reload).mockResolvedValue(undefined)
const close = jest.spyOn(window, "close").mockImplementation()
await import("@src/browser_action_popup")
await flush()
expect(document.querySelector("#state").textContent).toBe(
"Enabled globally",
)
expect(browser.runtime.sendMessage).toHaveBeenCalledWith({
type: "browser_action_background",
command: "getState",
args: [],
})
document.querySelector<HTMLElement>("#toggle").click()
expect(document.querySelector<HTMLButtonElement>("#reload").disabled).toBe(
true,
)
await flush()
expect(browser.runtime.sendMessage).toHaveBeenCalledWith({
type: "browser_action_background",
command: "toggle",
args: [],
})
expect(browser.tabs.reload).not.toHaveBeenCalled()
expect(document.querySelector("#state").textContent).toBe(
"Disabled globally",
)
document.querySelector<HTMLElement>("#reload").click()
expect(document.querySelector<HTMLButtonElement>("#toggle").disabled).toBe(
true,
)
await flush()
expect(browser.tabs.query).toHaveBeenCalledWith({
active: true,
currentWindow: true,
})
expect(browser.tabs.reload).toHaveBeenCalledWith(7)
expect(close).toHaveBeenCalled()
})

View file

@ -0,0 +1,63 @@
export {}
function message(command: "getState" | "toggle") {
return browser.runtime.sendMessage({
type: "browser_action_background",
command,
args: [],
})
}
const state = document.querySelector<HTMLElement>("#state")
const toggleButton = document.querySelector<HTMLButtonElement>("#toggle")
const reloadButton = document.querySelector<HTMLButtonElement>("#reload")
function showState(value) {
const disabled = value === "true"
state.textContent = disabled ? "Disabled globally" : "Enabled globally"
toggleButton.textContent = disabled
? "Enable Tridactyl"
: "Disable Tridactyl"
}
function showError(error) {
state.textContent = `Error: ${error instanceof Error ? error.message : error}`
}
function setBusy(busy) {
toggleButton.disabled = busy
reloadButton.disabled = busy
}
async function run(action: () => Promise<void>) {
setBusy(true)
try {
await action()
} catch (error) {
showError(error)
} finally {
setBusy(false)
}
}
message("getState")
.then(showState)
.then(() => setBusy(false), showError)
toggleButton.addEventListener("click", () =>
run(async () => {
showState(await message("toggle"))
}),
)
reloadButton.addEventListener("click", () =>
run(async () => {
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
})
if (tab?.id === undefined) throw new Error("No active tab found")
await browser.tabs.reload(tab.id)
window.close()
}),
)

View file

@ -12,6 +12,7 @@
*
* 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`.
*
* @packageDocumentation
*/
/** ignore this line */
@ -22,13 +23,20 @@ import { CompletionSourceFuse } from "@src/completions"
import { AproposCompletionSource } from "@src/completions/Apropos"
import { AutocmdCompletionSource } from "@src/completions/Autocmd"
import { BindingsCompletionSource } from "@src/completions/Bindings"
import { BmarkCompletionSource } from "@src/completions/Bmark"
import {
BmarkCompletionSource,
BookmarkFolderCompletionSource,
} from "@src/completions/Bmark"
import { CompositeCompletionSource } from "@src/completions/Composite"
import { ContainerCompletionSource } from "@src/completions/Container"
import { DialogCompletionSource } from "@src/completions/Dialog"
import { ExcmdCompletionSource } from "@src/completions/Excmd"
import { ExtensionsCompletionSource } from "@src/completions/Extensions"
import { FileSystemCompletionSource } from "@src/completions/FileSystem"
import { FindCompletionSource } from "@src/completions/Find"
import { GotoCompletionSource } from "@src/completions/Goto"
import { GuisetCompletionSource } from "@src/completions/Guiset"
import { GlossaryCompletionSource } from "@src/completions/Glossary"
import { HelpCompletionSource } from "@src/completions/Help"
import { HistoryCompletionSource } from "@src/completions/History"
import { PreferenceCompletionSource } from "@src/completions/Preferences"
@ -43,7 +51,7 @@ import { WindowCompletionSource } from "@src/completions/Window"
import { ProxyCompletionSource } from "@src/completions/Proxy"
import { contentState } from "@src/content/state_content"
import { theme } from "@src/content/styling"
import { getCommandlineFns } from "@src/lib/commandline_cmds"
import { expandAbbreviation, getCommandlineFns } from "@src/lib/commandline_cmds"
import * as tri_editor from "@src/lib/editor"
import "@src/lib/DANGEROUS-html-tagged-template"
import Logger from "@src/lib/logging"
@ -53,8 +61,13 @@ import * as genericParser from "@src/parsers/genericmode"
import * as perf from "@src/perf"
import state, * as State from "@src/state"
import * as R from "ramda"
import { MinimalKey, minimalKeyFromKeyboardEvent } from "@src/lib/keyseq"
import {
MinimalKey,
minimalKeyFromKeyboardEvent,
isTrustedKeyboardEvent,
} from "@src/lib/keyseq"
import { TabGroupCompletionSource } from "@src/completions/TabGroup"
import { ProfileCompletionSource } from "@src/completions/Profile"
/** @hidden **/
const logger = new Logger("cmdline")
@ -70,6 +83,11 @@ const commandline_state = {
completionsDiv: window.document.getElementById("completions"),
fns: undefined as ReturnType<typeof getCommandlineFns>,
getCompletion,
getCompletions: () =>
(commandline_state.activeCompletions || []).flatMap(source =>
source.visibleCompletions(),
),
getActiveCompletionSource,
history,
/** @hidden
* This is to handle Escape key which, while the cmdline is focused,
@ -82,6 +100,7 @@ const commandline_state = {
keyEvents: new Array<MinimalKey>(),
initialClInputValue: "",
refresh_completions,
resolveCloseWaiters,
state,
}
@ -95,39 +114,49 @@ function resizeArea() {
focus()
}
}
window.addEventListener("tridactyl-refresh-completions", resizeArea)
/** @hidden
* This is a bit loosely defined at the moment.
* Should work so long as there's only one completion source per prefix.
*/
function getCompletion(args_only = false) {
function getActiveCompletionSource(): CompletionSourceFuse | undefined {
if (!commandline_state.activeCompletions) return undefined
for (const comp of commandline_state.activeCompletions) {
if (comp.state === "normal" && comp.completion !== undefined) {
return args_only ? comp.args : comp.completion
}
}
return commandline_state.activeCompletions.filter(
({ state, completion }) =>
state === "normal" && completion !== undefined,
)[0]
}
/** @hidden **/
function getCompletion(args_only = false): string | undefined {
const activeSource = getActiveCompletionSource()
if (!activeSource) return undefined
return args_only ? activeSource.args : activeSource.completion
}
commandline_state.getCompletion = getCompletion
/** @hidden **/
export function enableCompletions() {
if (!commandline_state.activeCompletions) {
commandline_state.activeCompletions = [
AutocmdCompletionSource,
// FindCompletionSource,
FindCompletionSource,
BindingsCompletionSource,
BmarkCompletionSource,
BookmarkFolderCompletionSource,
TabAllCompletionSource,
BufferCompletionSource,
ExcmdCompletionSource,
ThemeCompletionSource,
TabHistoryCompletionSource,
CompositeCompletionSource,
ContainerCompletionSource,
DialogCompletionSource,
FileSystemCompletionSource,
GotoCompletionSource,
GuisetCompletionSource,
GlossaryCompletionSource,
HelpCompletionSource,
AproposCompletionSource,
HistoryCompletionSource,
@ -136,6 +165,7 @@ export function enableCompletions() {
SessionsCompletionSource,
SettingsCompletionSource,
TabGroupCompletionSource,
ProfileCompletionSource,
WindowCompletionSource,
ExtensionsCompletionSource,
ProxyCompletionSource,
@ -160,24 +190,44 @@ export function enableCompletions() {
/** @hidden **/
const noblur = () => setTimeout(() => commandline_state.clInput.focus(), 0)
/** @hidden **/
const closeWaiters: (() => void)[] = []
/** @hidden **/
function waitForClose() {
return new Promise<void>(resolve => closeWaiters.push(resolve))
}
/** @hidden **/
function resolveCloseWaiters() {
closeWaiters.splice(0).forEach(resolve => resolve())
}
/** @hidden **/
export function focus() {
function consumeBufferedPageKeys(bufferedPageKeys: string[]) {
const clInputStillFocused = window.document.activeElement === commandline_state.clInput;
logger.debug("stop_buffering_page_keys response received, bufferedPageKeys = ", bufferedPageKeys,
"clInputStillFocused = " + clInputStillFocused)
const clInputStillFocused =
window.document.activeElement === commandline_state.clInput
logger.debug(
"stop_buffering_page_keys response received, bufferedPageKeys = ",
bufferedPageKeys,
"clInputStillFocused = " + clInputStillFocused,
)
if (bufferedPageKeys.length !== 0) {
const currentClInputValue = commandline_state.clInput.value;
const initialClInputValue = commandline_state.initialClInputValue;
logger.debug("Consuming buffered page keys", bufferedPageKeys,
const currentClInputValue = commandline_state.clInput.value
const initialClInputValue = commandline_state.initialClInputValue
logger.debug(
"Consuming buffered page keys",
bufferedPageKeys,
"initialClInputValue = " + initialClInputValue,
"currentClInputValue = " + currentClInputValue);
"currentClInputValue = " + currentClInputValue,
)
// Native events are assumed to be character keydown events,
// i.e. characters appended at the end of clInput.
commandline_state.clInput.value =
initialClInputValue
+ bufferedPageKeys.join("")
+ currentClInputValue.substring(initialClInputValue.length)
initialClInputValue +
bufferedPageKeys.join("") +
currentClInputValue.substring(initialClInputValue.length)
// Update completion.
clInputValueChanged()
}
@ -185,8 +235,13 @@ export function focus() {
commandline_state.clInput.focus()
commandline_state.clInput.removeEventListener("blur", noblur)
commandline_state.clInput.addEventListener("blur", noblur)
logger.debug("commandline_frame clInput focus(), activeElement is clInput: " + (window.document.activeElement === commandline_state.clInput))
Messaging.messageOwnTab("stop_buffering_page_keys").then(consumeBufferedPageKeys)
logger.debug(
"commandline_frame clInput focus(), activeElement is clInput: " +
(window.document.activeElement === commandline_state.clInput),
)
Messaging.messageOwnTab("stop_buffering_page_keys").then(
consumeBufferedPageKeys,
)
}
/** @hidden **/
@ -195,25 +250,45 @@ let HISTORY_SEARCH_STRING: string
/** @hidden
* Command line keybindings
**/
const keyParser = keys => genericParser.parser("exmaps", keys)
const keyParser = keys => genericParser.parser("exmaps", keys, false)
/** @hidden **/
let history_called = false
/** @hidden **/
let prev_cmd_called_history = false
// Save programmer time by generating an immediately resolved promise
// eslint-disable-next-line @typescript-eslint/no-empty-function
const QUEUE: Promise<any>[] = [(async () => {})()]
let commandSession = { pending: 0, queue: [Promise.resolve()] }
const nativeInsertFallbacks = new Map<object, () => boolean>()
/** @hidden **/
commandline_state.clInput.addEventListener(
"keydown",
function (keyevent: KeyboardEvent) {
if (!keyevent.isTrusted) return
logger.debug("commandline_frame clInput keydown event listener", keyevent)
function (keyevent: Event) {
if (!isTrustedKeyboardEvent(keyevent)) return
logger.debug(
"commandline_frame clInput keydown event listener",
keyevent,
)
const session = commandSession
commandline_state.keyEvents.push(minimalKeyFromKeyboardEvent(keyevent))
const response = keyParser(commandline_state.keyEvents)
if (response.isMatch) {
const [funcname, ...args] = response.value?.startsWith("ex.")
? response.value.slice(3).split(/\s+/)
: []
const command =
commandline_state.fns[funcname as keyof typeof commandline_state.fns]
const nativeInsertFallback = nativeInsertFallbacks.get(command)
const commandArgument = args.length
? args.join(" ")
: nativeInsertFallback && keyevent.key.length === 1
? keyevent.key
: undefined
const insertCharacterNatively =
args.length === 0 &&
keyevent.key.length === 1 &&
!(keyevent.altKey || keyevent.ctrlKey || keyevent.metaKey) &&
session.pending === 0 &&
nativeInsertFallback?.()
if (response.isMatch && !insertCharacterNatively) {
keyevent.preventDefault()
keyevent.stopImmediatePropagation()
} else {
@ -227,25 +302,24 @@ commandline_state.clInput.addEventListener(
if (response.value) {
commandline_state.keyEvents = []
history_called = false
if (insertCharacterNatively) return
// If excmds start with 'ex.' they're coming back to us anyway, so skip that.
// 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, ...args] = response.value.slice(3).split(/\s+/)
if (funcname) {
session.pending++
QUEUE[QUEUE.length - 1].then(() => {
QUEUE.push(
session.queue[session.queue.length - 1].then(() => {
session.queue.push(
// Abuse async to wrap non-promises in a promise
// eslint-disable-next-line @typescript-eslint/require-await
(async () =>
commandline_state.fns[
funcname as keyof typeof commandline_state.fns
](
args.length === 0 ? undefined : args.join(" "),
))(),
(async () => session === commandSession && command(commandArgument))()
.catch(error => void logger.error(error))
.finally(() => session.pending--),
)
prev_cmd_called_history = history_called
if (session === commandSession)
prev_cmd_called_history = history_called
})
} else {
// Send excmds directly to our own tab, which fixes the
@ -258,6 +332,7 @@ commandline_state.clInput.addEventListener(
// to be a touch less latency-sensitive.
Messaging.messageOwnTab("controller_content", "acceptExCmd", [
response.value,
"commandline",
]).then(_ => (prev_cmd_called_history = history_called))
}
} else {
@ -267,50 +342,80 @@ commandline_state.clInput.addEventListener(
true,
)
let refreshQueue: Promise<unknown> = Promise.resolve()
export function refresh_completions(exstr) {
const session = commandSession
const result = refreshQueue.then(() =>
session === commandSession ? refreshCompletions(exstr) : undefined,
)
refreshQueue = result.catch(() => undefined)
return result
}
function refreshCompletions(exstr) {
if (!commandline_state.activeCompletions) enableCompletions()
// We can't use the regular logging mechanism because the user is using the command line.
return Promise.all(
commandline_state.activeCompletions.map(comp =>
comp.filter(exstr).then(() => {
if (comp.shouldRefresh()) {
return resizeArea()
}
}),
comp
.filter(exstr)
.then(() => {
if (comp.shouldRefresh()) {
return resizeArea()
}
})
.catch(err => console.error(err)),
),
).catch(err => {
console.error(err)
return []
}) // We can't use the regular logging mechanism because the user is using the command line.
)
}
/** @hidden **/
let onInputPromise: Promise<any> = Promise.resolve()
let onInputPromise: Promise<void | void[]> = Promise.resolve()
const COMPLETION_THROTTLE_MS = 100
let completionTimer
let lastCompletionStarted = -Infinity
/** @hidden **/
commandline_state.clInput.addEventListener("input", () => {
logger.debug("commandline_frame clInput input event listener")
clInputValueChanged();
clInputValueChanged()
})
/** @hidden **/
async function updateCompletions(exstr: string, session = commandSession) {
lastCompletionStarted = performance.now()
await onInputPromise
if (session !== commandSession) return
if (exstr !== commandline_state.clInput.value) {
contentState.cmdline_filter = exstr
return
}
onInputPromise = refresh_completions(exstr)
onInputPromise.then(() => {
contentState.cmdline_filter = exstr
})
}
/** @hidden **/
function clInputValueChanged() {
const exstr = commandline_state.clInput.value
const session = commandSession
contentState.current_cmdline = exstr
contentState.cmdline_filter = ""
// 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)
setTimeout(async () => {
// Make sure the previous computation has ended
await onInputPromise
// If we're not the current completion computation anymore, stop
if (exstr !== commandline_state.clInput.value) {
contentState.cmdline_filter = exstr
return
}
onInputPromise = refresh_completions(exstr)
onInputPromise.then(() => {
contentState.cmdline_filter = exstr
})
}, 100)
// Run immediately when idle, otherwise retain one trailing refresh.
clearTimeout(completionTimer)
const delay =
COMPLETION_THROTTLE_MS - (performance.now() - lastCompletionStarted)
if (delay <= 0) void updateCompletions(exstr, session)
else
completionTimer = setTimeout(
() =>
void updateCompletions(
commandline_state.clInput.value,
session,
),
delay,
)
}
/** @hidden **/
@ -322,6 +427,12 @@ let cmdline_history_current = ""
* Otherwise, no need to pass an argument.
*/
export function clear(evlistener = false) {
if (evlistener) {
commandSession = { pending: 0, queue: [Promise.resolve()] }
clearTimeout(completionTimer)
lastCompletionStarted = -Infinity
}
if (evlistener) prev_cmd_called_history = false
if (evlistener)
commandline_state.clInput.removeEventListener("blur", noblur)
commandline_state.clInput.value = ""
@ -352,6 +463,7 @@ async function history(n) {
const pot_history = matches[clamped_ind]
commandline_state.clInput.value =
pot_history === undefined ? cmdline_history_current : pot_history
clInputValueChanged()
// if there was no clampage, update history position
// there's a more sensible way of doing this but that would require more programmer time
@ -369,19 +481,29 @@ export function fillcmdline(
newcommand?: string,
trailspace = true,
ffocus = true,
wait = false,
) {
logger.debug("commandline_frame fillcmdline(newcommand = " + newcommand + " trailspace = " + trailspace + " ffocus = " + ffocus + ")")
logger.debug(
"commandline_frame fillcmdline(newcommand = " +
newcommand +
" trailspace = " +
trailspace +
" ffocus = " +
ffocus +
")",
)
if (trailspace) commandline_state.clInput.value = newcommand + " "
else commandline_state.clInput.value = newcommand
commandline_state.initialClInputValue = commandline_state.clInput.value
commandline_state.isVisible = true
const closed = wait ? waitForClose() : undefined
let result = Promise.resolve([])
// Focus is lost for some reason.
if (ffocus) {
focus()
result = refresh_completions(commandline_state.clInput.value)
}
return result
return wait ? result.then(() => closed) : result
}
/** @hidden **/
@ -406,7 +528,17 @@ export function editor_function(fn_name: keyof typeof tri_editor, ...args) {
Messaging.addListener("commandline_frame", Messaging.attributeCaller(SELF))
logger.debug("Added commandline_frame message listener")
commandline_state.fns = getCommandlineFns(commandline_state)
/** @namespace */
export const commandlineFns = getCommandlineFns(commandline_state)
commandline_state.fns = commandlineFns
nativeInsertFallbacks.set(
commandline_state.fns.insert_character_or_completion,
() => {
if (getCompletion()) return false
expandAbbreviation(commandline_state.clInput)
return true
},
)
Messaging.addListener(
"commandline_cmd",
Messaging.attributeCaller(commandline_state.fns),
@ -417,8 +549,8 @@ Messaging.addListener(
// 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.
;(window as any).tri = Object.assign(window.tri || {}, {
window["tri"] = Object.assign(window.tri || {}, {
perfObserver: perf.listenForCounters(),
})
Messaging.messageOwnTab("commandline_frame_ready_to_receive_messages")
Messaging.messageOwnTab("commandline_frame_ready_to_receive_messages", window.name)

View file

@ -11,11 +11,14 @@ Concrete completion classes have been moved to src/completions/.
*/
import Fuse from "fuse.js"
import { enumerate } from "@src/lib/itertools"
import { toNumber } from "@src/lib/convert"
import * as aliases from "@src/lib/aliases"
import { backoff } from "@src/lib/patience"
import * as config from "@src/lib/config"
export { decodeUrlForDisplay } from "@src/lib/url_util"
export function treePrefix(level: number) {
return ` ${"".repeat(Math.max(level - 1, 0))}${level ? "┌─" : ""}· `
}
export const DEFAULT_FAVICON = browser.runtime.getURL(
"static/defaultFavicon.svg",
@ -33,16 +36,17 @@ export abstract class CompletionOption {
}
export abstract class CompletionSource {
readonly options: CompletionOption[]
options: CompletionOption[]
node: HTMLElement
public completion: string
public args: string
public trailingSpace: boolean
protected prefixes: string[] = []
protected lastFocused: CompletionOption
private _state: OptionState
private _prevState: OptionState
constructor(prefixes) {
constructor(prefixes, options = { trailingSpace: true }) {
const commands = aliases.getCmdAliasMapping()
// Now, for each prefix given as argument, add it to the completionsource's prefix list and also add any alias it has
@ -56,6 +60,11 @@ export abstract class CompletionSource {
// Not sure this is necessary but every completion source has it
this.prefixes = this.prefixes.map(p => p + " ")
this.trailingSpace = options.trailingSpace
}
protected canonicalisePrefix(prefix: string) {
return aliases.expandExstr(prefix).trim()
}
/** Control presentation of Source */
@ -145,14 +154,13 @@ export interface CompletionOptionFuse extends CompletionOptionHTML {
}
export interface ScoredOption {
index: number
option: CompletionOptionFuse
score: number
}
export abstract class CompletionSourceFuse extends CompletionSource {
options: CompletionOptionFuse[]
public node
public options: CompletionOptionFuse[]
fuseOptions = {
keys: ["fuseKeys"],
@ -174,11 +182,22 @@ export abstract class CompletionSourceFuse extends CompletionSource {
protected optionContainer = html`<table class="optionContainer"></table>`
constructor(prefixes, className: string, title?: string) {
super(prefixes)
this.node = html`<div class="${className} hidden">
<div class="sectionHeader">${title || className}</div>
</div>`
private fusedOptions: CompletionOptionFuse[]
constructor(
prefixes,
className: string,
title?: string | HTMLElement,
options = { trailingSpace: true },
) {
super(prefixes, options)
this.node = html`<div class="${className} hidden"></div>`
const header =
typeof title === "string" || title === undefined
? html`<div>${title || className}</div>`
: title
header.classList.add("sectionHeader")
this.node.appendChild(header)
this.node.appendChild(this.optionContainer)
this.state = "hidden"
}
@ -224,6 +243,18 @@ export abstract class CompletionSourceFuse extends CompletionSource {
this.updateDisplay()
}
completionForOption(option: CompletionOption) {
const [prefix] = this.splitOnPrefix(this.lastExstr)
return prefix ? [prefix, option.value].join(" ") : option.value
}
visibleCompletions() {
if (this.state === "hidden") return []
return (this.options || [])
.filter(option => option.state !== "hidden")
.map(option => this.completionForOption(option))
}
select(option: CompletionOption) {
if (this.lastExstr !== undefined && option !== undefined) {
const [prefix] = this.splitOnPrefix(this.lastExstr)
@ -248,20 +279,15 @@ export abstract class CompletionSourceFuse extends CompletionSource {
/** Rtn sorted array of {option, score} */
scoredOptions(query: string): ScoredOption[] {
const searchThis = this.options.map((elem, index) => ({
index,
fuseKeys: elem.fuseKeys,
if (this.fuse === undefined || this.fusedOptions !== this.options) {
this.fuse = new Fuse(this.options, this.fuseOptions)
this.fusedOptions = this.options
}
return this.fuse.search(query).map(result => ({
option: result.item,
score: result.score,
}))
this.fuse = new Fuse(searchThis, this.fuseOptions)
return this.fuse.search(query).map(result => {
// console.log(result, result.item, query)
const index = toNumber(result.item.index)
return {
index,
option: this.options[index],
score: result.score as number,
}
})
}
/** Set option state by score
@ -270,44 +296,35 @@ export abstract class CompletionSourceFuse extends CompletionSource {
focus the best match.
*/
setStateFromScore(scoredOpts: ScoredOption[], autoselect = false) {
const matches = scoredOpts.map(res => res.index)
const matches = new Set(scoredOpts.map(res => res.option))
const hidden_options = []
for (const [index, option] of enumerate(this.options)) {
if (matches.includes(index)) option.state = "normal"
else {
for (const option of this.options) {
if (matches.has(option)) {
option.state = "normal"
} else {
option.state = "hidden"
hidden_options.push(option)
}
}
// ideally, this would not deselect anything unless it fell off the list of matches
if (matches.length && autoselect) {
this.select(this.options[matches[0]])
if (scoredOpts.length && autoselect) {
this.select(scoredOpts[0].option)
} else {
this.deselect()
}
// sort this.options by score
if (this.sortScoredOptions) {
const sorted_options = matches.map(index => this.options[index])
const sorted_options = scoredOpts.map(res => res.option)
this.options = sorted_options.concat(hidden_options)
this.fusedOptions = this.options
}
}
/** Call to replace the current display */
updateDisplay() {
const newContainer = this.optionContainer.cloneNode(
false,
) as HTMLElement
for (const option of this.options) {
if (option.state !== "hidden")
// This is probably slow: `.html` means the HTML parser will be invoked
newContainer.appendChild(option.html)
}
this.optionContainer.replaceWith(newContainer)
this.optionContainer = newContainer
const visibleOptions = this.options.filter(o => o.state !== "hidden").map(o => o.html)
this.optionContainer.replaceChildren(...visibleOptions)
this.next(0)
}
@ -322,7 +339,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
this.deselect()
// visopts.length + 1 because we want an empty completion at the end
const max = visopts.length + 1
const opt = visopts[(currind + inc + max) % max]
const opt = visopts[((currind + inc) % max + max) % max]
if (opt) this.select(opt)
return true
})
@ -332,7 +349,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
/* abstract onUpdate(query: string, prefix: string, options: CompletionOptionFuse[]) */
// Lots of methods don't need this but some do
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars-experimental
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
async onInput(exstr: string) {}
}

View file

@ -1,13 +1,22 @@
import * as Completions from "@src/completions"
import * as Metadata from "@src/.metadata.generated"
import {
excmdsFunctions,
defaultConfigMembers,
getDoc,
memberDoc,
} from "@src/.metadata.generated"
import * as aliases from "@src/lib/aliases"
import * as config from "@src/lib/config"
import { glossaryOptions } from "@src/completions/Glossary"
class AproposCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
class AproposCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public name: string, doc: string, flag: string) {
constructor(
public name: string,
doc: string,
flag: string,
) {
super()
this.value = `${flag} ${name}`
this.html = html`<tr class="AproposCompletionOption option">
@ -20,12 +29,21 @@ class AproposCompletionOption extends Completions.CompletionOptionHTML
export class AproposCompletionSource extends Completions.CompletionSourceFuse {
public options: AproposCompletionOption[]
constructor(private _parent) {
super(["apropos"], "AproposCompletionSource", "Apropos")
constructor(
private _parent,
prefixes = ["apropos"],
className = "AproposCompletionSource",
title = "Apropos",
) {
super(prefixes, className, title)
this._parent.appendChild(this.node)
}
protected createOption(name: string, doc: string, flag: string) {
return new AproposCompletionOption(name, doc, flag)
}
public async filter(exstr: string) {
this.lastExstr = exstr
this.completion = undefined
@ -42,20 +60,13 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
return
}
const file = Metadata.everything.getFile("src/lib/config.ts")
const default_config = file.getClass("default_config")
const excmds = Metadata.everything.getFile("src/excmds.ts")
const fns = excmds.getFunctions()
const settings = config.get()
const exaliases = settings.exaliases
const bindings = settings.nmaps
if (
fns === undefined ||
exaliases === undefined ||
bindings === undefined
) {
if (exaliases === undefined || bindings === undefined) {
return
}
const fns = Object.entries(excmdsFunctions)
const flags = {
"-a": (options, query) =>
@ -65,17 +76,15 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
(
alias +
aliases.expandExstr(alias) +
excmds.getFunction(aliases.expandExstr(alias))
excmdsFunctions[aliases.expandExstr(alias)]
)
.toLowerCase()
.includes(query),
)
.map(alias => {
const cmd = aliases.expandExstr(alias)
const doc =
(excmds.getFunction(cmd) || ({} as any)).doc ||
""
return new AproposCompletionOption(
const doc = getDoc(excmdsFunctions[cmd])
return this.createOption(
alias,
`Alias for \`${cmd}\`. ${doc}`,
"-a",
@ -92,7 +101,7 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
)
.map(
binding =>
new AproposCompletionOption(
this.createOption(
binding,
`Normal mode binding for \`${bindings[binding]}\``,
"-b",
@ -102,16 +111,14 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
"-e": (options, query) =>
options.concat(
fns
.filter(
([name, fn]) =>
!fn.hidden &&
(name + fn.doc).toLowerCase().includes(query),
.filter(([name, fn]) =>
(name + getDoc(fn)).toLowerCase().includes(query),
)
.map(
([name, fn]) =>
new AproposCompletionOption(
this.createOption(
name,
`Excmd. ${fn.doc}`,
`Excmd. ${getDoc(fn)}`,
"-e",
),
),
@ -120,23 +127,23 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
options.concat(
Object.keys(settings)
.filter(x =>
(x + default_config.getMember(x)?.doc)
(x + memberDoc(defaultConfigMembers[x]))
.toLowerCase()
.includes(query),
)
.map(setting => {
const member = default_config.getMember(setting)
let doc = ""
if (member !== undefined) {
doc = member.doc
}
return new AproposCompletionOption(
const doc = memberDoc(defaultConfigMembers[setting])
return this.createOption(
setting,
`Setting. ${doc}`,
"-s",
)
}),
),
"-g": (options, query) =>
options.concat(
glossaryOptions(this.createOption.bind(this), query, false),
),
}
const args = query.split(" ")

View file

@ -25,7 +25,7 @@ export class AutocmdCompletionSource extends Completions.CompletionSourceFuse {
constructor(private _parent) {
super(
["autocmd", "autocmddelete"],
["autocmd", "autocmddelete", "autocontaindelete"],
"AutocmdCompletionSource",
"Autocommands",
)
@ -59,7 +59,16 @@ export class AutocmdCompletionSource extends Completions.CompletionSourceFuse {
this.state = "hidden"
return
}
const is_autocmddelete = /del/.test(prefix)
const command = this.canonicalisePrefix(prefix)
if (command === "autocontaindelete") {
this.options = Object.entries(config.get("autocontain"))
.sort(([a], [b]) => a.localeCompare(b))
.map(([pattern, container]) =>
new AutocmdCompletionOption(pattern, container, `-u ${pattern}`),
)
return this.updateChain()
}
const is_autocmddelete = command === "autocmddelete"
const filter_defined_autocmds = is_autocmddelete
const defined_autocmds = config.get("autocmds")
// Config may contain empty dictionnaries if user deleted all patterns

View file

@ -2,6 +2,17 @@ import * as Completions from "@src/completions"
import * as config from "@src/lib/config"
import * as Binding from "@src/lib/binding"
const modeDescriptions = new Map([
["normal", "Default mode"],
["ignore", "Almost all keys passed through to web pages except these binds"],
["insert", "Editing text"],
["input", "Bindings after `gi` focuses a text field"],
["ex", "Command line binds"],
["hint", "Binds accessible during hint mode"],
["visual", "Text selection outside insert mode"],
["browser", "Binds accessible everywhere, including while pages load. NB: some caveats, see :help bind"],
])
class BindingsCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
@ -51,6 +62,7 @@ export class BindingsCompletionSource extends Completions.CompletionSourceFuse {
this.state = "hidden"
return
}
prefix = this.canonicalisePrefix(prefix)
this.deselect()
@ -90,7 +102,7 @@ export class BindingsCompletionSource extends Completions.CompletionSourceFuse {
options + "--mode=" + name,
{
name,
value: "",
value: modeDescriptions.get(name),
mode: "Mode Name",
},
),

View file

@ -9,7 +9,7 @@ class BmarkCompletionOption
constructor(
public value: string,
bmark: browser.bookmarks.BookmarkTreeNode,
bmark: providers.Bookmark,
) {
super()
if (!bmark.title) {
@ -17,14 +17,14 @@ class BmarkCompletionOption
}
// Push properties we want to fuzmatch on
this.fuseKeys.push(bmark.title, bmark.url)
this.fuseKeys.push(bmark.path, bmark.title, bmark.url)
this.html = html`<tr class="BmarkCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td class="title">${bmark.title}</td>
<td class="title">${bmark.path}${bmark.title}</td>
<td class="content">
<a class="url" target="_blank" href=${bmark.url}
>${bmark.url}</a
>${Completions.decodeUrlForDisplay(bmark.url)}</a
>
</td>
</tr>`
@ -70,6 +70,11 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
option += " "
query = args.slice(2).join(" ")
}
if (query.startsWith("-b")) {
const args = query.split(" ")
option += args.slice(0, 1).join(" ") + " "
query = args.slice(1).join(" ")
}
this.completion = undefined
this.options = (await providers.getBookmarks(query))
@ -96,14 +101,57 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
// Call concrete class
return this.updateDisplay()
}
}
select(option: Completions.CompletionOption) {
if (this.lastExstr !== undefined && option !== undefined) {
this.completion = "bmarks " + option.value
option.state = "focused"
this.lastFocused = option
} else {
throw new Error("lastExstr and option must be defined!")
export class BookmarkFolderCompletionSource extends Completions.CompletionSourceFuse {
constructor() {
super(["bmark"], "BookmarkFolderCompletionSource", "Bookmark Folders", {
trailingSpace: false,
})
}
async onInput(exstr: string) {
const [_command, _url, path] = this.parseArgs(exstr)
if (path == undefined) {
this.options = undefined
return
}
this.options = (await providers.getBookmarkFolders(path))
.slice(0, 10)
.map(path => new BookmarkFolderCompletionOption(path))
}
splitOnPrefix(exstr: string): string[] {
const [command, url, path] = this.parseArgs(exstr)
return [`${command} ${url}`, path]
}
private parseArgs(exstr: string): string[] {
const [command, args] = super.splitOnPrefix(exstr)
if (!args) {
return [command]
}
const spaceIndex = args.search(/\s+/)
const url = args.slice(0, spaceIndex)
if (spaceIndex == -1) {
return [command, url]
}
const path = args.slice(spaceIndex + 1)
return [command, url, path]
}
}
class BookmarkFolderCompletionOption
extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
fuseKeys = []
constructor(public value: string) {
super()
this.fuseKeys.push(value)
this.html = html`<tr class="BookmarkFolderCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td class="title">${value}</td>
</tr>`
}
}

View file

@ -1,11 +1,10 @@
import * as Completions from "@src/completions"
import * as ExcmdCompletions from "@src/completions/Excmd"
import * as Metadata from "@src/.metadata.generated"
import { excmdsFunctions, getDoc } from "@src/.metadata.generated"
import * as config from "@src/lib/config"
import * as aliases from "@src/lib/aliases"
const PREFIX = "composite"
const regex = new RegExp("^" + PREFIX + " ")
// Most of this is copied verbatim from Excmd.ts - would have liked to inherit but constructor posed difficulties
export class CompositeCompletionSource extends Completions.CompletionSourceFuse {
@ -27,7 +26,7 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
return this.updateOptions(exstr)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
// eslint-disable-next-line @typescript-eslint/no-unused-vars
updateChain(exstr = this.lastExstr, options = this.options) {
if (this.options.length > 0) this.state = "normal"
else this.state = "hidden"
@ -35,6 +34,15 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
this.updateDisplay()
}
completionForOption(option: ExcmdCompletions.ExcmdCompletionOption) {
return (
this.lastExstr.replace(
new RegExp(this.getendexstr(this.lastExstr) + "$"),
"",
) + option.value
)
}
select(option: ExcmdCompletions.ExcmdCompletionOption) {
this.completion =
this.lastExstr.replace(
@ -65,21 +73,15 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
return
}
const excmds = Metadata.everything.getFile("src/excmds.ts")
if (!excmds) return
const fns = excmds.getFunctions()
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
this.options = this.scoreOptions(
fns
.filter(
([name, fn]) => !fn.hidden && name.startsWith(end_exstr),
)
Object.entries(excmdsFunctions)
.filter(([name]) => name.startsWith(end_exstr))
.map(
([name, fn]) =>
new ExcmdCompletions.ExcmdCompletionOption(
name,
fn.doc,
getDoc(fn),
),
),
)
@ -90,12 +92,12 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
)
for (const alias of exaliases) {
const cmd = aliases.expandExstr(alias)
const fn = excmds.getFunction(cmd)
const fn = excmdsFunctions[cmd]
if (fn) {
this.options.push(
new ExcmdCompletions.ExcmdCompletionOption(
alias,
`Alias for \`${cmd}\`. ${fn.doc}`,
`Alias for \`${cmd}\`. ${getDoc(fn)}`,
),
)
} else {
@ -118,8 +120,8 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
}
private getendexstr(exstr) {
return exstr
.replace(regex, "")
const [, query = exstr] = this.splitOnPrefix(exstr)
return query
.split("|")
.slice(-1)[0]
.split(";")

View file

@ -0,0 +1,37 @@
import * as Completions from "@src/completions"
import * as Containers from "@src/lib/containers"
class ContainerCompletionOption
extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public value: string) {
super()
this.fuseKeys.push(value)
this.html = html`<tr class="ContainerCompletionOption option">
<td class="title">${value}</td>
</tr>`
}
}
export class ContainerCompletionSource extends Completions.CompletionSourceFuse {
public options: ContainerCompletionOption[]
constructor(private _parent) {
super(["recontain", "containerclose", "containerdelete", "containerupdate"], "ContainerCompletionSource", "Containers")
this._parent.appendChild(this.node)
}
async onInput(exstr: string) {
const [prefix, query] = this.splitOnPrefix(exstr)
if (!prefix) return
const command = this.canonicalisePrefix(prefix)
this.options =
command === "containerupdate" && /\s/u.test(query)
? undefined
: (await Containers.getAll())
.filter(container => command !== "containerupdate" || !/\s/u.test(container.name))
.map(container => new ContainerCompletionOption(container.name))
}
}

30
src/completions/Dialog.ts Normal file
View file

@ -0,0 +1,30 @@
import * as Completions from "@src/completions"
import { ABOUT_PAGES } from "@src/lib/about_pages"
import "@src/lib/DANGEROUS-html-tagged-template"
class DialogCompletionOption
extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys: string[]
constructor(public value: string, description: string) {
super()
this.fuseKeys = [value, description]
this.html = html`<tr class="DialogCompletionOption option">
<td class="title">${value}</td>
<td class="description"></td>
</tr>`
this.html.querySelector(".description").textContent = description
}
}
export class DialogCompletionSource extends Completions.CompletionSourceFuse {
public options = Object.entries(ABOUT_PAGES).map(
([page, description]) => new DialogCompletionOption(page, description),
)
constructor(parent: HTMLElement) {
super(["dialog"], "DialogCompletionSource", "Firefox about pages")
parent.appendChild(this.node)
}
}

View file

@ -1,14 +1,16 @@
import * as Completions from "@src/completions"
import * as Metadata from "@src/.metadata.generated"
import { excmdsFunctions, getDoc } from "@src/.metadata.generated"
import * as config from "@src/lib/config"
import * as aliases from "@src/lib/aliases"
export class ExcmdCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
export class ExcmdCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public value: string, public documentation: string = "") {
constructor(
public value: string,
public documentation = "",
) {
super()
this.fuseKeys.push(this.value)
this.fuseKeys.push(this.value, this.documentation)
// Create HTMLElement
this.html = html`<tr class="ExcmdCompletionOption option">
@ -37,7 +39,7 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
return this.updateOptions(exstr)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
// eslint-disable-next-line @typescript-eslint/no-unused-vars
updateChain(exstr = this.lastExstr, options = this.options) {
if (this.options.length > 0) this.state = "normal"
else this.state = "hidden"
@ -57,16 +59,15 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
private async updateOptions(exstr = "") {
this.lastExstr = exstr
const excmds = Metadata.everything.getFile("src/excmds.ts")
if (!excmds) return
const fns = excmds.getFunctions()
this.node.querySelector(".sectionHeader").textContent = "ex commands"
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
this.options = this.scoreOptions(
fns
.filter(([name, fn]) => !fn.hidden && name.startsWith(exstr))
.map(([name, fn]) => new ExcmdCompletionOption(name, fn.doc)),
Object.entries(excmdsFunctions)
.filter(([name]) => name.startsWith(exstr))
.map(
([name, fn]) => new ExcmdCompletionOption(name, getDoc(fn)),
),
)
// Also narrow down aliases map to possible completions
@ -80,13 +81,13 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
for (const alias of Object.keys(exaliases)) {
const cmd = aliases.expandExstr(alias, exaliases)
const fn = excmds.getFunction(cmd)
const fn = excmdsFunctions[cmd]
if (fn) {
this.options.push(
new ExcmdCompletionOption(
alias,
`Alias for \`${cmd}\`. ${fn.doc}`,
`Alias for \`${cmd}\`. ${getDoc(fn)}`,
),
)
} else {
@ -100,15 +101,35 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
// Add partial matched funcs like: 'conf' ~= 'viewconfig'
const seen = new Set(this.options.map(o => o.value))
const partial_options = this.scoreOptions(
fns
.filter(
([name, fn]) =>
!fn.hidden && name.includes(exstr) && !seen.has(name),
)
.map(([name, fn]) => new ExcmdCompletionOption(name, fn.doc)),
Object.entries(excmdsFunctions)
.filter(([name]) => name.includes(exstr) && !seen.has(name))
.map(
([name, fn]) => new ExcmdCompletionOption(name, getDoc(fn)),
),
)
this.options = this.options.concat(partial_options)
const [command] = exstr.trim().split(/\s+/)
if (
this.options.length === 0 &&
command &&
!excmdsFunctions[command] &&
exaliasesConfig[command] === undefined
) {
const query = exstr.toLowerCase()
this.node.querySelector(".sectionHeader").textContent = "ex commands (no matches, falling back to doc search)"
this.options = this.scoreOptions(
Object.entries(excmdsFunctions)
.filter(([, fn]) =>
getDoc(fn).toLowerCase().includes(query),
)
.map(
([name, fn]) =>
new ExcmdCompletionOption(name, getDoc(fn)),
),
)
}
this.options.forEach(o => (o.state = "normal"))
return this.updateChain()
}

View file

@ -7,6 +7,7 @@ class ExtensionsCompletionOption extends Completions.CompletionOptionHTML
constructor(public name: string, public optionsUrl: string) {
super()
this.value = name
this.fuseKeys.push(this.name)
this.html = html`<tr class="option">
@ -60,12 +61,6 @@ export class ExtensionsCompletionSource extends Completions.CompletionSourceFuse
return this.updateDisplay()
}
select(option: ExtensionsCompletionOption) {
this.completion = "extoptions " + option.name
option.state = "focused"
this.lastFocused = option
}
private scoreOptions(options: ExtensionsCompletionOption[]) {
return options.sort((o1, o2) => o1.name.localeCompare(o2.name))
}

View file

@ -39,7 +39,10 @@ export class FileSystemCompletionSource extends Completions.CompletionSourceFuse
}
let [cmd, path] = this.splitOnPrefix(exstr)
if (cmd === undefined) {
if (
cmd === undefined ||
(cmd === "source" && /^--url(?:\s|$)/.test(path))
) {
this.state = "hidden"
return
}

View file

@ -1,116 +1,171 @@
import { activeTabId } from "@src/lib/webext"
import * as Messaging from "@src/lib/messaging"
import * as Completions from "../completions"
import * as config from "@src/lib/config"
import * as Messaging from "@src/lib/messaging"
import { ownTabId } from "@src/lib/webext"
class FindCompletionOption
extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(m, reverse = false) {
super()
this.value =
(reverse ? "-? " : "") + ("-: " + m.index) + " " + m.rangeData.text
this.fuseKeys.push(m.rangeData.text)
public fuseKeys
// Create HTMLElement
constructor(index: number, match, args: string) {
super()
this.value = [`--jump-to ${index}`, args].filter(Boolean).join(" ")
this.fuseKeys = [match.text, match.precontext, match.postcontext, match.breadcrumbs]
this.html = html`<tr class="FindCompletionOption option">
<td class="breadcrumbs">${match.breadcrumbs}</td>
<td class="content">
${m.precontext}<span class="match">${m.rangeData.text}</span
>${m.postcontext}
${match.precontext}<span class="match">${match.text}</span
>${match.postcontext}
</td>
<td class="position">${match.position}</td>
</tr>`
}
}
export class FindCompletionSource extends Completions.CompletionSourceFuse {
public options: FindCompletionOption[]
public prevCompletion = null
public completionCount = 0
public options: FindCompletionOption[] = []
private session = Math.random()
private tabId = ownTabId()
private active = false
private request = 0
private findArgs = ""
private pending: Promise<void> = Promise.resolve()
private selection: Promise<void> = Promise.resolve()
constructor(private _parent) {
super(["find "], "FindCompletionSource", "Matches")
this._parent.appendChild(this.node)
}
async onInput(exstr) {
const id = this.completionCount++
// If there's already a promise being executed, wait for it to finish
await this.prevCompletion
// Since we might have awaited for this.prevCompletion, we don't have a guarantee we're the last completion the user asked for anymore
if (id === this.completionCount - 1) {
// If we are the last completion
this.prevCompletion = this.updateOptions(exstr)
await this.prevCompletion
}
}
// Overriding this function is important, the default one has a tendency to hide options when you don't expect it
setStateFromScore() {
this.options.forEach(o => (o.state = "normal"))
}
private async updateOptions(exstr?: string) {
if (!exstr) return
// Flag parsing because -? should reverse completions
const tokens = exstr.split(" ")
const flagpos = tokens.indexOf("-?")
const reverse = flagpos >= 0
if (reverse) {
tokens.splice(flagpos, 1)
}
const query = tokens.slice(1).join(" ")
const minincsearchlen = await config.getAsync("minincsearchlen")
// No point if continuing if the user hasn't started searching yet
if (query.length < minincsearchlen) return
let findresults = await config.getAsync("findresults")
const incsearch = (await config.getAsync("incsearch")) === "true"
if (findresults === 0 && !incsearch) return
let incsearchonly = false
if (findresults === 0) {
findresults = 1
incsearchonly = true
}
// Note: the use of activeTabId here might break completions if the user starts searching for a pattern in a really big page and then switches to another tab.
// Getting the tabId should probably be done in the constructor but you can't have async constructors.
const tabId = await activeTabId()
const findings = await Messaging.messageTab(
tabId,
"finding_content",
"find",
[query, findresults, reverse],
constructor(_parent?) {
super(
["find"],
"FindCompletionSource",
html`<table><tr>
<td class="breadcrumbs">Breadcrumbs</td>
<td class="content">Context</td>
<td class="position">Position</td>
</tr></table>`,
)
}
// If the search was successful
if (findings.length > 0) {
// Get match context
const len = await config.getAsync("findcontextlen")
const matches = await Messaging.messageTab(
tabId,
"finding_content",
"getMatches",
[findings, len],
)
if (incsearch)
Messaging.messageTab(tabId, "finding_content", "jumpToMatch", [
query,
false,
0,
])
if (!incsearchonly) {
this.options = matches.map(
m => new FindCompletionOption(m, reverse),
filter(exstr: string) {
if (exstr === this.lastExstr && this.completion) return Promise.resolve()
this.lastExstr = exstr
const [, argstr] = this.splitOnPrefix(exstr)
if (argstr === undefined) return this.cancel()
const request = ++this.request
this.findArgs = argstr.trim()
this.active = true
this.options = []
this.optionContainer.replaceChildren()
this.state = "hidden"
const optionArgs = this.findArgs.split(/(?:^|\s)--(?:\s|$)/, 1)[0]
if (/(^|\s)(--jump-to|-:)=?$/.test(optionArgs)) return this.cancel()
const hasJump = /(^|\s)(--jump-to(?:=|\s)|-:(?:\s|$))/.test(optionArgs)
const regex = /(^|\s)(-r|--regex)(?=\s|$)/.test(optionArgs)
const delay = regex
? new Promise<void>(resolve => window.setTimeout(resolve, 250))
: Promise.resolve()
this.pending = delay
.then(() => {
if (request !== this.request) return
return this.send(
{ session: this.session, completions: !hasJump },
...this.findArgs.split(/\s+/),
)
this.updateChain(exstr, this.options)
}
})
.then(matches => {
if (request !== this.request) return
this.options = hasJump
? []
: (matches || []).map(
match =>
new FindCompletionOption(
match.index,
match,
this.findArgs,
),
)
if (this.options.length) this.updateChain(exstr, this.options)
else this.state = "hidden"
this.resize()
})
.catch(() => {
if (request !== this.request) return
this.options = []
this.state = "hidden"
this.resize()
return this.send({ session: this.session, cancel: true })
})
return Promise.resolve()
}
setStateFromScore() {
this.options.forEach(option => (option.state = "normal"))
this.deselect()
}
scoredOptions() {
return []
}
updateDisplay() {
this.optionContainer.replaceChildren(
...this.options
.filter(option => option.state !== "hidden")
.map(option => option.html),
)
this.next(0)
}
select(option: FindCompletionOption) {
super.select(option)
this.selection = this.preview(option.value, true)
}
async next(inc = 1) {
if (!this.active) return false
const pending = this.pending
if (inc !== 0) await this.pending
if (!this.active || pending !== this.pending) return false
const moved = await super.next(inc)
if (inc !== 0 && moved) {
if (!this.completion) this.selection = this.preview(this.findArgs)
await this.selection
}
return moved
}
destroy() {
return this.cancel()
}
private cancel() {
if (!this.active) return Promise.resolve()
++this.request
this.active = false
this.options = []
this.state = "hidden"
return this.send({ session: this.session, cancel: true }).catch(
() => undefined,
)
}
private preview(args: string, selected = false) {
return this.send(
{ session: this.session, completions: false, selected },
...args.split(/\s+/),
)
.then(() => undefined)
.catch(() => undefined)
}
private send(preview, ...args: string[]) {
return this.tabId.then(tabId =>
Messaging.messageTab(tabId, "excmd_content", "find", [
preview,
...args,
]),
)
}
private resize() {
window.dispatchEvent(new Event("tridactyl-refresh-completions"))
}
}

View file

@ -0,0 +1,31 @@
import * as Completions from "@src/completions"
import { ExcmdCompletionOption } from "@src/completions/Excmd"
import glossary from "@src/.glossary.generated.json"
export function glossaryOptions(createOption, query: string, prefix: boolean) {
const needle = query.toLowerCase()
return glossary
.filter(entry =>
prefix
? entry.word.toLowerCase().startsWith(needle)
: (entry.word + entry.definition)
.toLowerCase()
.includes(needle),
)
.map(entry =>
createOption(entry.word, `Glossary. ${entry.definition}`, "-g"),
)
}
export class GlossaryCompletionSource extends Completions.CompletionSourceFuse {
public options: ExcmdCompletionOption[]
constructor(parent) {
super(["define"], "GlossaryCompletionSource", "Glossary")
this.options = glossary.map(
entry => new ExcmdCompletionOption(entry.word, entry.definition),
)
this.sortScoredOptions = true
parent.appendChild(this.node)
}
}

View file

@ -12,7 +12,7 @@ class GotoCompletionOption
this.fuseKeys.push(title)
this.html = html`<tr class="GotoCompletionOption option">
<td class="title" style="padding-left: ${level * 4}ch">${title}</td>
<td class="title">${Completions.treePrefix(level)}${title}</td>
</tr>`
}
}

View file

@ -1,13 +1,23 @@
import * as Completions from "@src/completions"
import * as Metadata from "@src/.metadata.generated"
import { AproposCompletionSource } from "@src/completions/Apropos"
import {
excmdsFunctions,
defaultConfigMembers,
getDoc,
memberDoc,
} from "@src/.metadata.generated"
import * as aliases from "@src/lib/aliases"
import * as config from "@src/lib/config"
import { glossaryOptions } from "@src/completions/Glossary"
class HelpCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
class HelpCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(public name: string, doc: string, flag: string) {
constructor(
public name: string,
doc: string,
flag: string,
) {
super()
this.value = `${flag} ${name}`
this.html = html`<tr class="HelpCompletionOption option">
@ -17,13 +27,15 @@ class HelpCompletionOption extends Completions.CompletionOptionHTML
}
}
export class HelpCompletionSource extends Completions.CompletionSourceFuse {
export class HelpCompletionSource extends AproposCompletionSource {
public options: HelpCompletionOption[]
constructor(private _parent) {
super(["help"], "HelpCompletionSource", "Help")
constructor(_parent) {
super(_parent, ["help"], "HelpCompletionSource", "Help")
}
this._parent.appendChild(this.node)
protected createOption(name: string, doc: string, flag: string) {
return new HelpCompletionOption(name, doc, flag)
}
public async filter(exstr: string) {
@ -41,21 +53,15 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
this.state = "hidden"
return
}
this.node.querySelector(".sectionHeader").textContent = "Help (prefix matches)"
const file = Metadata.everything.getFile("src/lib/config.ts")
const default_config = file.getClass("default_config")
const excmds = Metadata.everything.getFile("src/excmds.ts")
const fns = excmds.getFunctions()
const settings = config.get()
const exaliases = settings.exaliases
const bindings = settings.nmaps
if (
fns === undefined ||
exaliases === undefined ||
bindings === undefined
) {
if (exaliases === undefined || bindings === undefined) {
return
}
const fns = Object.entries(excmdsFunctions)
const flags = {
"-a": (options, query) =>
@ -64,9 +70,7 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
.filter(alias => alias.startsWith(query))
.map(alias => {
const cmd = aliases.expandExstr(alias)
const doc =
(excmds.getFunction(cmd) || ({} as any)).doc ||
""
const doc = getDoc(excmdsFunctions[cmd])
return new HelpCompletionOption(
alias,
`Alias for \`${cmd}\`. ${doc}`,
@ -90,15 +94,12 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
"-e": (options, query) =>
options.concat(
fns
.filter(
([name, fn]) =>
!fn.hidden && name.startsWith(query),
)
.filter(([name]) => name.startsWith(query))
.map(
([name, fn]) =>
new HelpCompletionOption(
name,
`Excmd. ${fn.doc}`,
`Excmd. ${getDoc(fn)}`,
"-e",
),
),
@ -108,11 +109,7 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
Object.keys(settings)
.filter(x => x.startsWith(query))
.map(setting => {
const member = default_config.getMember(setting)
let doc = ""
if (member !== undefined) {
doc = member.doc
}
const doc = memberDoc(defaultConfigMembers[setting])
return new HelpCompletionOption(
setting,
`Setting. ${doc}`,
@ -120,6 +117,10 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
)
}),
),
"-g": (options, query) =>
options.concat(
glossaryOptions(this.createOption.bind(this), query, true),
),
}
const args = query.split(" ")
@ -133,18 +134,15 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
)
}
if (opts.length === 0) {
this.node.querySelector(".sectionHeader").textContent = "Help (prefix match failed, showing :apropos matches)"
return super.filter(exstr)
}
this.options = opts
this.options.sort((compopt1, compopt2) =>
compopt1.name.localeCompare(compopt2.name),
)
return this.updateChain()
}
updateChain() {
// Options are pre-trimmed to the right length.
this.options.forEach(option => (option.state = "normal"))
// Call concrete class
return this.updateDisplay()
}
}

View file

@ -18,7 +18,8 @@ class HistoryCompletionOption
let preplain = page.bmark ? "B" : ""
preplain += page.search ? "S" : ""
let pre = preplain
if (config.get("completions", "Tab", "statusstylepretty") === "true") {
// Tab settings apply here to grandfather in people who set it before when there was a bug
if ((config.get("completions", "Tab", "statusstylepretty") === "true") || (config.get("completions", "History", "statusstylepretty") === "true")) {
pre = page.bmark ? "\u2B50" : ""
pre += page.search ? "\u{1F50D}" : ""
}
@ -33,7 +34,7 @@ class HistoryCompletionOption
<td class="title">${page.title}</td>
<td class="content">
${page.search ? "Search " : ""}
<a class="url" target="_blank" href=${page.url}>${page.url}</a>
<a class="url" target="_blank" href=${page.url}>${Completions.decodeUrlForDisplay(page.url)}</a>
</td>
</tr>`
}
@ -71,6 +72,7 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
}
const headerPostfix = []
prefix = this.canonicalisePrefix(prefix)
// Ignoring command-specific arguments
// It's terrible but it's ok because it's just a stopgap until an actual commandline-parsing API is implemented
@ -138,6 +140,13 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
break
}
}
if (
this.completion === undefined &&
this.options.length > 0 &&
config.get("completions", "History", "autoselect") === "true"
) {
this.select(this.options[0])
}
return this.updateDisplay()
}
@ -147,7 +156,10 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
updateChain() {}
private async scoreOptions(query: string, n: number) {
if (!query || config.get("historyresults") === 0) {
if (
(!query && config.get("historysort") === "frequency") ||
config.get("historyresults") === 0
) {
return (await providers.getTopSites()).slice(0, n)
} else {
return (await providers.getCombinedHistoryBmarks(query)).slice(0, n)

Some files were not shown because too many files have changed in this diff Show more