Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b987685d89 |
@@ -35,17 +35,6 @@ Firefox Desktop is the only current implementation and release target. Do not ad
|
||||
|
||||
Use two-space indentation in JSON, YAML, and JavaScript configuration. Preserve logical key grouping in `manifest.json`. Use lowercase, hyphenated filenames and include dimensions in icon names. Keep changes focused and avoid adding runtime dependencies without a concrete need.
|
||||
|
||||
## Options Page
|
||||
|
||||
- Keep every setting. Customization in a lightweight extension is what sets
|
||||
this one apart, so simplify wording and layout, but never remove a setting
|
||||
without the maintainer's agreement. Less-used settings can sit under
|
||||
**More options**.
|
||||
- Write hotkeys as `<Ctrl>+L`: named keys in angle brackets, single characters
|
||||
plain. `options/shortcut.js` swaps in Mac key names.
|
||||
- Say that ChatGPT opens with the text filled in, never that the text is sent,
|
||||
searched, or asked. The options design spec explains why.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Unit tests cover the pure logic in `lib/` via `node:test`; automated checks
|
||||
@@ -57,12 +46,10 @@ After `npm test`, use `npm run dev` and confirm:
|
||||
|
||||
- `gpt <query>` opens ChatGPT, the provider stays non-default, and Firefox
|
||||
search settings can configure it.
|
||||
- The Options/Preferences tab renders the drawings, every setting (including
|
||||
those under More options), and Restore defaults,
|
||||
- The Options/Preferences tab renders the settings plus Restore defaults,
|
||||
in both light and dark themes and at a narrow window width, and values
|
||||
persist across a restart. Expanding and collapsing More options and the
|
||||
collapsed sections resizes the inline panel without clipping or scrolling.
|
||||
- **Right-click menu → Simple** shows the direct **Ask ChatGPT** item only for
|
||||
persist across a restart.
|
||||
- **Context menu → Simple** shows the direct **Ask ChatGPT** item only for
|
||||
selected text. **Advanced** shows the contextual submenu; selection takes
|
||||
precedence over links. **Off** hides the menu. Switch modes without restarting
|
||||
and confirm Alt+Shift+G still works in every mode. Restore defaults returns
|
||||
@@ -87,7 +74,7 @@ After `npm test`, use `npm run dev` and confirm:
|
||||
stale cross-origin frame silently seeding the prompt was the worse failure.
|
||||
- Click the **Ask ChatGPT** toolbar button with no selection, a top-frame
|
||||
selection, and an iframe selection after a stale top-frame selection.
|
||||
Check restricted-page fallback, right-click menu Off, saved prompt and opening
|
||||
Check restricted-page fallback, context menu Off, saved prompt and opening
|
||||
preferences, and consent before shortening long selections. Test both the
|
||||
pinned toolbar button and its entry in Firefox’s extensions menu.
|
||||
|
||||
|
||||
@@ -2,24 +2,6 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version).
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Replace the license with the Zavage Software Freeware License 1.0, written in
|
||||
plain English. Anyone may install and use the extension free of charge, for
|
||||
any purpose, and the new terms also cover earlier versions. Republishing it,
|
||||
sharing changed versions or reusing its code still needs permission.
|
||||
|
||||
## [1.3.0] (2026-10-18)
|
||||
|
||||
- Use the extension's bundled icon for the ChatGPT search engine.
|
||||
- Rewrite the options page for first-time users: usage instructions with a
|
||||
small drawing for each way to use the extension, a preview of the Advanced
|
||||
right-click menu, plainer wording, and the prompt template, temporary chat,
|
||||
and web search settings under More options. Hotkeys are written as
|
||||
`<Ctrl>+L`, with Mac key names on a Mac.
|
||||
- Correct the keyword instructions: a custom keyword works alongside `gpt`
|
||||
rather than replacing it.
|
||||
|
||||
## [1.2.0] (2026-10-18)
|
||||
|
||||
The first update on addons.mozilla.org since 1.0. It also delivers everything
|
||||
|
||||
+53
-42
@@ -1,70 +1,81 @@
|
||||
Zavage Software Freeware License 1.0
|
||||
1. Grant of License
|
||||
|
||||
Copyright (c) 2024-2026 Mathew Guest <mat@zavage.net> (https://zavage-software.com), who publishes as Zavage Software ("Zavage Software").
|
||||
This Zavage Software inc. License Agreement (the "Agreement") is a legal agreement between
|
||||
you (either an individual or a single entity) and Zavage Software inc. ("Licensor") for the
|
||||
use of the software product identified above, which includes computer software and may include
|
||||
associated media, printed materials, and "online" or electronic documentation (the "Software").
|
||||
|
||||
Anyone may install and use this extension free of charge, for any purpose. That permission lasts forever for every version this license covers. Without permission, nobody may republish, sell or give away copies of it, changed or not, or reuse its code, text or images elsewhere. You may read its source code, but this isn't an open source license. The full terms follow.
|
||||
By installing, copying, or otherwise using the Software,
|
||||
you agree to be bound by the terms of this Agreement. If
|
||||
you do not agree to the terms of this Agreement, do not
|
||||
install or use the Software.
|
||||
|
||||
This license covers the browser extension Search with ChatGPT powered by OpenAI! (the "Software"): every version Zavage Software made before this license or publishes with it, under any name and for any browser, together with their source code, icons, images, documentation and store listings. On addons.mozilla.org its add-on ID is {24644f0c-bfcb-4ebd-a16b-bc8a7d154288}. Anything in the Software or its repository history that belongs to someone else, such as another company's logo, isn't covered by this license and stays with its owner. In this license, "you" means the person or organization using the Software.
|
||||
|
||||
Section 1: What you may do
|
||||
2. License
|
||||
|
||||
You may install and use the Software for any purpose, personal or commercial, on any number of devices, free of charge. You may install it for others too, on devices you manage, such as your family's or your organization's. You may also:
|
||||
The Software is licensed, not sold. Subject to the terms of this Agreement,
|
||||
Licensor grants you a limited, non-exclusive, non-transferable,
|
||||
non-sublicensable license to use the Software solely for your
|
||||
internal business purposes.
|
||||
|
||||
- make the copies you need to install, run, sync and back up the Software, including a copy on an internal server or network share for installing it on devices you manage;
|
||||
- read, study and test its source code, including to check its security and privacy, and make the copies you need for that, such as a clone of the official repository; and
|
||||
- change your own copy, and use, copy and share the changed version only within your own household or organization.
|
||||
3. Restrictions
|
||||
|
||||
These permissions are worldwide, permanent and irrevocable: as long as you follow section 2, Zavage Software can't take them back. Section 5 explains what happens if you don't.
|
||||
You may not:
|
||||
|
||||
Section 2: What you may not do
|
||||
Copy, modify, or create derivative works of the Software.
|
||||
Distribute, sell, sublicense, lease, rent, or otherwise transfer the Software to any third party.
|
||||
Reverse engineer, decompile, disassemble, or otherwise attempt to derive the source code of the Software,
|
||||
except to the extent expressly permitted by applicable law.
|
||||
Use the Software in any manner that could damage, disable, overburden, or impair the
|
||||
Software or interfere with any other party's use of the Software.
|
||||
|
||||
Except as this license allows, and unless Zavage Software gives you written permission (an email is enough), you may not:
|
||||
4. Ownership
|
||||
|
||||
- publish, distribute, sell, rent or give the Software, or a version made from it (a fork), to anyone else, whether or not you charge for it, including by listing it on an add-on store (such as addons.mozilla.org, the Chrome Web Store or Microsoft Edge Add-ons) or posting it on a public website or in a public code repository. A fork includes a changed copy, a version for another browser, a userscript, and a translation of its code or text, whether made by hand, with AI or with other tools;
|
||||
- copy or adapt any part of its code, text, icons or images for anything else, such as another add-on, app, script, website, service or store listing; or
|
||||
- remove or change its copyright and license notices.
|
||||
All right, title, and interest in and to the Software, including all intellectual
|
||||
property rights, are and will remain the sole property of Licensor. This Agreement does
|
||||
not grant you any rights in or to the Software other than the limited license granted above.
|
||||
|
||||
Sharing a link to the official listing or repository is always fine, and so is naming the Software, quoting short parts of it, or showing its icon and screenshots or videos of it in use, in a review, an article, a tutorial, a class, a security report or a support request. Nothing in this license limits rights the law gives you anyway, such as fair use or fair dealing. Zavage Software keeps every right this license doesn't give you.
|
||||
|
||||
Section 3: Names and logos
|
||||
5. Termination
|
||||
|
||||
You may use the Zavage Software name and the Software's name and icon to refer to the Software, but not to brand anything else or to suggest that Zavage Software made or endorsed something it didn't. ChatGPT and OpenAI are trademarks of OpenAI. The Software is unofficial and isn't affiliated with or endorsed by OpenAI.
|
||||
This Agreement is effective until terminated. Licensor may terminate this Agreement
|
||||
immediately if you breach any term of this Agreement. Upon termination, you must cease
|
||||
all use of the Software and destroy all copies of the Software in your possession or control.
|
||||
|
||||
Section 4: ChatGPT is a separate service
|
||||
|
||||
The Software opens chatgpt.com in your browser with your text filled in. OpenAI runs ChatGPT under its own terms and privacy policy. Zavage Software doesn't control ChatGPT and isn't responsible for its availability or its answers, or for how OpenAI handles text that reaches ChatGPT through the Software.
|
||||
6. Disclaimer of Warranties
|
||||
|
||||
Section 5: If you break section 2
|
||||
The Software is provided "AS IS" and "AS AVAILABLE," without warranty of any kind.
|
||||
Licensor disclaims all warranties, whether express, implied, statutory, or
|
||||
otherwise, including but not limited to the implied warranties of merchantability,
|
||||
fitness for a particular purpose, title, and non-infringement.
|
||||
|
||||
If you break section 2, Zavage Software may tell you so in writing (an email is enough). If, within 30 days of that notice, you stop and undo what you can, such as taking down copies you published, you keep your permissions under this license, though that doesn't excuse the breach. If you don't, or if you break section 2 again after a notice, your permissions under this license end: stop using the Software and delete your copies. Sections 6 to 10 still apply after that.
|
||||
|
||||
Section 6: No warranty
|
||||
7. Limitation of Liability
|
||||
|
||||
Because the Software is free, it comes without a warranty:
|
||||
To the maximum extent permitted by applicable law, in no event will Licensor be liable
|
||||
for any indirect, incidental, special, consequential, or punitive damages, or any loss of
|
||||
profits or revenues, whether incurred directly or indirectly, or any loss of data, use,
|
||||
goodwill, or other intangible losses, resulting from (a) your use of or inability to use
|
||||
the Software; (b) any unauthorized access to or use of the Software; or (c) any other
|
||||
matter relating to the Software.
|
||||
|
||||
TO THE EXTENT THE LAW ALLOWS, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT.
|
||||
|
||||
Section 7: Limit of liability
|
||||
8. Governing Law
|
||||
|
||||
TO THE EXTENT THE LAW ALLOWS, FOR ANY CLAIM ARISING FROM THE SOFTWARE OR THIS LICENSE, ZAVAGE SOFTWARE IS NOT LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR PUNITIVE DAMAGES, OR FOR LOST DATA, PROFITS OR GOODWILL, AND ITS TOTAL LIABILITY FOR ALL SUCH CLAIMS WILL NOT EXCEED US$50.
|
||||
This Agreement will be governed by and construed in accordance with the laws of Denver, CO and the United States,
|
||||
without regard to its conflict of law principles.
|
||||
|
||||
Sections 6 and 7 don't limit liability for death or personal injury caused by negligence, for fraud, for harm caused deliberately, recklessly or through gross negligence, or under product liability laws. They also don't limit any other liability, or take away any consumer right, that the law doesn't allow to be limited or taken away.
|
||||
|
||||
Section 8: Law
|
||||
9. Entire Agreement
|
||||
|
||||
Colorado law and United States federal law govern this license. If you use the Software as a consumer, outside your trade, business or profession, you also keep the protections of the law where you live that an agreement can't take away.
|
||||
This Agreement constitutes the entire agreement between you and Licensor regarding the use of the
|
||||
Software and supersedes all prior and contemporaneous understandings, agreements, representations,
|
||||
and warranties, both written and oral.
|
||||
|
||||
Section 9: Earlier and later versions
|
||||
10. Contact Information
|
||||
|
||||
This license replaces any other terms that came with the versions it covers. A later version may come with different terms, but they can't take back what this license gives you for the versions it covers.
|
||||
If you have any questions about this Agreement, please contact Zavage Software Inc. at:
|
||||
|
||||
Section 10: General
|
||||
|
||||
- If a court won't enforce part of this license, the rest still applies.
|
||||
- If the Software passes to someone else, such as a company, this license keeps applying and they take Zavage Software's place in it.
|
||||
|
||||
Section 11: Contact
|
||||
|
||||
Official listing: https://addons.mozilla.org/firefox/addon/search-with-chatgpt/
|
||||
Official repository: https://git-repos.zavage.net/zavage-software/search-with-chatgpt-powered-by-openai-extension
|
||||
Questions, permission requests or security reports: mat@zavage.net, https://zavage-software.com
|
||||
* mat@zavage.net (email)
|
||||
* https://zavage-software.com (website)
|
||||
|
||||
@@ -7,7 +7,7 @@ It uses no API key, custom server, or intermediary handler.
|
||||
**ChatGPT may require you to sign in. If your prompt is not sent automatically,
|
||||
press Enter or click Send in ChatGPT.**
|
||||
|
||||
This is an unofficial extension by Zavage Software.
|
||||
This is an unofficial extension provided by Zavage Software Inc.
|
||||
|
||||
- [Firefox Add-ons listing](https://addons.mozilla.org/en-US/firefox/addon/search-with-chatgpt/)
|
||||
- [Project website](https://zavage-software.com/portfolio/search-with-ChatGPT)
|
||||
@@ -15,9 +15,7 @@ This is an unofficial extension by Zavage Software.
|
||||
- [Feedback or report a bug](https://zavage-software.com/contact-us)
|
||||
- [Zavage Software](https://zavage-software.com)
|
||||
|
||||
Anyone may use the extension free of charge, for any purpose. Republishing it,
|
||||
sharing changed versions or reusing its code needs permission. You may read the
|
||||
source, but it isn't open source. See `LICENSE.txt` for the full terms.
|
||||
See `LICENSE.txt` for licensing terms.
|
||||
|
||||
## Ask about selected text
|
||||
|
||||
@@ -33,24 +31,23 @@ Click the extension’s **Ask ChatGPT** toolbar button to use selected text or,
|
||||
with nothing selected, open a blank chat. On pages Firefox does not allow
|
||||
extensions to read, the button opens a blank chat. You can pin the button to
|
||||
the toolbar from Firefox’s extensions menu. It uses your saved prompt and
|
||||
tab/window preferences, and works even when the right-click menu is Off.
|
||||
tab/window preferences, and works even when the context menu is Off.
|
||||
|
||||
The options panel's collapsed **Change the keyboard shortcut** section shows
|
||||
your currently configured shortcut and refreshes when it changes or you return
|
||||
to the panel. If you remove it, the panel shows
|
||||
The options panel shows your currently configured shortcut and refreshes when
|
||||
it changes or you return to the panel. If you remove it, the panel shows
|
||||
“No keyboard shortcut assigned”. Its **Change keyboard shortcut** button opens
|
||||
Firefox’s shortcut editor when supported; otherwise follow the menu path above.
|
||||
**Restore defaults** does not change your keyboard shortcut.
|
||||
|
||||
To customize the prompt, open **More options** and set **Prompt template** to something like
|
||||
To customize the prompt, set **Prompt template** to something like
|
||||
`Explain simply: {query}` or `Translate into Spanish: {query}`. The Simple menu action, Ask normally, toolbar button, and shortcut use your template.
|
||||
Other advanced actions use their own dedicated prompts.
|
||||
|
||||
## Search keyword and default engine
|
||||
|
||||
Open Firefox’s **Settings → Search**, or paste `about:preferences#search` into
|
||||
the address bar. Under **Search Shortcuts**, edit ChatGPT’s keyword to add
|
||||
your own prefix; `gpt` keeps working alongside it. Under **Default Search Engine**, choose
|
||||
the address bar. Under **Search Shortcuts**, edit ChatGPT’s keyword to replace
|
||||
`gpt` with your preferred prefix. Under **Default Search Engine**, choose
|
||||
ChatGPT to search without a prefix, or choose your previous engine to switch
|
||||
back. See [Firefox’s search settings guide](https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox)
|
||||
for illustrated instructions.
|
||||
@@ -63,25 +60,21 @@ not change them.
|
||||
Open `about:addons`, select this extension, and choose the **Options** tab
|
||||
(**Preferences** on macOS and Linux). Available settings:
|
||||
|
||||
- **Right-click menu** — **Simple** (default) keeps the direct selection action;
|
||||
- **Context menu** — **Simple** (default) keeps the direct selection action;
|
||||
**Advanced** offers contextual presets; **Off** hides the menu. The toolbar
|
||||
button and keyboard shortcut work in every mode. Existing enabled/disabled preferences migrate
|
||||
to Simple/Off. Changes apply without restarting Firefox.
|
||||
- **Show Translate in the Advanced menu** — off by default. When enabled, choose
|
||||
- **Show Translate in advanced menu** — off by default. When enabled, choose
|
||||
**Ask each time** for a language dialog, or enter a **Preferred language**
|
||||
to translate directly. An empty or invalid preferred language falls back to
|
||||
asking. Enter continues; Escape or Cancel opens nothing.
|
||||
- **Open ChatGPT in** — a new tab, a new background tab, or a new window.
|
||||
|
||||
Under **More options**, which opens by itself once you have changed one of
|
||||
them:
|
||||
|
||||
- **Prompt template** — wraps your text, for example `Explain simply: {query}`.
|
||||
Up to 1,000 characters.
|
||||
- **Request a temporary chat** — asks ChatGPT not to save the conversation.
|
||||
ChatGPT may not honour this; check its own indicator before relying on it.
|
||||
- **Ask ChatGPT to use web search** — a hint to ChatGPT's interface, not a
|
||||
guarantee.
|
||||
- **Open in** — a new tab, a new background tab, or a new window.
|
||||
- **Prompt template** — wraps your text, for example `Explain simply: {query}`.
|
||||
Up to 1,000 characters.
|
||||
|
||||
Restore defaults resets extension preferences to Simple mode with translation
|
||||
hidden; it does not change Firefox's search engine or shortcut settings.
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# Options page for first-time users
|
||||
|
||||
Status: implemented on `feat/options-page-cleanup` (PR #23).
|
||||
|
||||
## Goal
|
||||
|
||||
Make the options page usable by people who are not comfortable with computers,
|
||||
without losing any setting. Customization in a lightweight extension is what
|
||||
sets this one apart, so the page simplifies wording and layout, never features.
|
||||
|
||||
## Layout
|
||||
|
||||
1. **Description**, no heading: "This extension adds ChatGPT as a Firefox
|
||||
search engine."
|
||||
2. **Usage instructions**: one labelled line per way to open ChatGPT, each with
|
||||
one drawing:
|
||||
- Address bar
|
||||
- Right-click
|
||||
- Keyboard (hidden when no shortcut is assigned)
|
||||
- Toolbar
|
||||
|
||||
A small tip follows: ChatGPT opens with the text filled in, press
|
||||
`<Enter>` if it isn't sent, and sign-in may be needed.
|
||||
3. **Settings**:
|
||||
- Visible: **Right-click menu** (renamed from "Context menu"), its
|
||||
Translate sub-settings, and **Open ChatGPT in**.
|
||||
- Choosing **Advanced** reveals a drawing of the Ask ChatGPT submenu. It
|
||||
sits in the block that only shows for Advanced, so it previews the one
|
||||
setting newcomers find hardest to picture, at no cost to anyone else.
|
||||
- Under **More options**: prompt template, temporary chat, and web search.
|
||||
The summary names all three. The section opens on load, or when a sync
|
||||
changes one of them, if any differs from its default. That way a
|
||||
customised setting never looks lost.
|
||||
- **Restore defaults** sits on the right in red, because it discards every
|
||||
customised setting at once.
|
||||
4. Two collapsed sections:
|
||||
- **Use your own keyword or make ChatGPT your default search engine**, as
|
||||
numbered steps.
|
||||
- **Change the keyboard shortcut**.
|
||||
5. Project links, centred, stacked on narrow panels.
|
||||
|
||||
## Drawings, not screenshots
|
||||
|
||||
Each drawing is HTML, CSS, and a little inline SVG. The only image is the
|
||||
bundled extension icon. The drawings are `aria-hidden`; the text next to them
|
||||
carries the instructions.
|
||||
|
||||
Real screenshots were considered and rejected for the page:
|
||||
- they go stale when Firefox redesigns its address bar or menus;
|
||||
- the right-click menu looks different on Windows, macOS, and Linux;
|
||||
- they need separate light and dark versions;
|
||||
- they must be bundled, since loading them remotely would repeat the problem
|
||||
fixed in #20.
|
||||
|
||||
Size was not the issue. Screenshots would have added roughly 50–150 KB. The
|
||||
whole rewrite, drawings included, added under 4 KB; the package went from
|
||||
52.8 KB to 56.5 KB.
|
||||
Real screenshots belong on the AMO listing instead, which has none.
|
||||
|
||||
The address-bar drawing shows a ChatGPT label after `gpt` and a space. The
|
||||
basis is the code shipped in Firefox 157.0.1, read from its `omni.ja`; it has
|
||||
not yet been checked on screen:
|
||||
- `UrlbarProviderAliasEngines` gives keyword results a `keyword` payload;
|
||||
- `UrlbarInputBase.onFirstResult` enters search mode for such a result.
|
||||
|
||||
If Firefox shows no label, the drawing should show plain text instead.
|
||||
|
||||
The keyboard drawing draws key caps for the user's actual shortcut, which
|
||||
`options/shortcut.js` builds from `commands.getAll()`.
|
||||
|
||||
## Hotkeys
|
||||
|
||||
Hotkeys are written as `<Ctrl>+L`: named keys in angle brackets, single
|
||||
characters plain. This is the maintainer's own convention from the AMO
|
||||
listing. It applies to the static text and to the live shortcut.
|
||||
|
||||
On macOS the page uses Mac key names, detected with
|
||||
`runtime.getPlatformInfo()`, which needs no permission:
|
||||
- `Ctrl` and `Command` become `Cmd`; Firefox reads a manifest's `Ctrl` as
|
||||
Command there (`ShortcutUtils.chromeModifierKeyMap`).
|
||||
- `MacCtrl` becomes `Ctrl`.
|
||||
- `Alt` becomes `Option`.
|
||||
- `Enter` becomes `Return`.
|
||||
|
||||
Shortcuts set in Firefox's shortcut editor on macOS are stored with
|
||||
`MacCtrl`, `Alt`, `Command`, and `Shift` (`addon-shortcuts.mjs`,
|
||||
`getShortcutForEvent`).
|
||||
|
||||
## Copy rules kept
|
||||
|
||||
These come from the
|
||||
[options design](2026-07-29-extension-options-design.md):
|
||||
- Copy says ChatGPT opens with the text filled in, never that it is sent,
|
||||
searched, or asked.
|
||||
- The temporary-chat label and warning are unchanged, with no "private",
|
||||
"incognito", or "secure" near them.
|
||||
- Web search stays labelled as a hint.
|
||||
|
||||
## Testing
|
||||
|
||||
`npm test` covers:
|
||||
- the key formatter, including the macOS key names;
|
||||
- the static hotkeys in `options.html` matching the formatter;
|
||||
- the key caps drawn for the live shortcut.
|
||||
|
||||
Manual checks are listed in `AGENTS.md`.
|
||||
|
||||
To preview the page outside the extension:
|
||||
1. Copy `options/`, `lib/`, and `icons/` to a scratch folder.
|
||||
2. Add a script before `options.js` that defines a stand-in `browser` with
|
||||
`runtime.getPlatformInfo`, `commands`, and `storage`.
|
||||
3. Serve the folder on localhost.
|
||||
4. Run `firefox --headless -no-remote -profile <tmp> --screenshot out.png
|
||||
--window-size=680,1600 <url>`.
|
||||
|
||||
For dark mode, give the throwaway profile
|
||||
`layout.css.prefers-color-scheme.content-override` = 0 and
|
||||
`ui.systemUsesDarkTheme` = 1. This renders with Firefox's own engine, but not
|
||||
inside `about:addons`.
|
||||
|
||||
## Not in scope
|
||||
|
||||
- Screenshots and an updated description for the AMO listing.
|
||||
- Pinning the toolbar button by default (`browser_action.default_area`).
|
||||
- Rewriting the README's hotkeys in the `<Alt>+<Shift>+G` style.
|
||||
@@ -1,119 +0,0 @@
|
||||
# Replace the license
|
||||
|
||||
Status: implemented on `docs/replace-license`. Researched and reviewed, but not
|
||||
legal advice; no lawyer has read it.
|
||||
|
||||
## Problem
|
||||
|
||||
The old `LICENSE.txt` was a stock business EULA, unchanged since June 2024. It:
|
||||
|
||||
- allowed use only "for your internal business purposes", which leaves out
|
||||
people using the extension at home;
|
||||
- banned copying and reverse engineering, although the source is public and the
|
||||
listing links to it;
|
||||
- named "Zavage Software inc." as licensor, though the code is Mathew's;
|
||||
- said users agreed by installing, which binds no one who installs from AMO.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Anyone may use the extension for free, for any purpose, forever.
|
||||
2. Nobody may fork it, republish it, or build or sell anything from it.
|
||||
3. Everything else is reserved.
|
||||
|
||||
The text must not scare ordinary users away.
|
||||
|
||||
## How it was reviewed
|
||||
|
||||
Three multi-agent passes on 2026-10-19. Each finding was checked against primary
|
||||
sources by a separate skeptic.
|
||||
|
||||
1. Seven lenses: US law, international law, platforms, comparable licenses, a
|
||||
would-be cloner, ordinary users and drafting. A completeness critic followed.
|
||||
This pass produced 92 findings.
|
||||
2. A four-lens adversarial review of the revision, which produced 29 findings.
|
||||
3. A final consistency check and cold read.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Custom text.** No standard license fits all three goals:
|
||||
- PolyForm Strict allows noncommercial use only.
|
||||
- PolyForm Internal Use covers business use only.
|
||||
- PolyForm Perimeter, Shield and Noncommercial, Elastic, BSL and FSL all allow
|
||||
redistribution.
|
||||
- CC BY-NC-ND allows sharing unchanged copies.
|
||||
- **A permission, not an agreement.** Copyright already forbids forks without
|
||||
anyone agreeing to anything. "By installing you agree" doesn't bind people who
|
||||
install from a store (Specht v. Netscape, 2d Cir. 2002).
|
||||
- **Licensor: Mathew Guest, publishing as Zavage Software.** Colorado lists a
|
||||
Zavage Software Inc. LLC, but the code was never assigned to it. Section 10
|
||||
keeps the license in force if the code later passes to a company.
|
||||
- **Earlier versions are covered.** On AMO, versions 1.0, 1.2 and 1.3 share one
|
||||
license record (9155), and their signed packages bundle the old EULA. Section 9
|
||||
replaces those terms.
|
||||
- **Each version stays free forever.** A later version may carry different
|
||||
terms, but it can't take back what this license gives. The owner chose this
|
||||
over promising that every future version stays free.
|
||||
- **Changed copies stay within your household or organization.** There is no
|
||||
route for having Mozilla sign a changed copy. Submitting to AMO makes the
|
||||
submitter warrant worldwide distribution rights (Firefox Add-on Distribution
|
||||
Agreement §5(c)), which this license withholds. Changed copies still run as
|
||||
temporary add-ons, or in Developer Edition, Nightly and ESR.
|
||||
- **Reviews are welcome.** Reviews, tutorials, classes and security reports may
|
||||
name the extension, quote short parts, and show its icon and screenshots of it
|
||||
in use.
|
||||
- **Liability is capped at US$50**, for claims about the Software only. The cap
|
||||
has explicit exceptions (injury, fraud, deliberate or reckless harm, gross
|
||||
negligence, product liability). A bare "to the extent the law allows" doesn't
|
||||
save a cap in Germany, the UK or the EU.
|
||||
- **Clauses left out:**
|
||||
- A courts clause, which binds no forker and is unfair to EU and UK consumers.
|
||||
- A noncompete or a ban on similar extensions. Ideas aren't protected, and
|
||||
such a clause risks copyright misuse.
|
||||
- Clauses on AI training and on outside contributions.
|
||||
- **The name is "Freeware", not "Free Use".** "Free Use" has a slang meaning, and
|
||||
"free" alone suggests free software.
|
||||
- **The layout is plain text.** AMO shows license text as plain text: every line
|
||||
break shows, and there's no Markdown. So each paragraph sits on one line, and
|
||||
headings read "Section N:".
|
||||
|
||||
## Publishing on AMO (untested, 2026-10-19)
|
||||
|
||||
1. In the Developer Hub, open the add-on's **Manage Authors & License** page.
|
||||
2. Set "What is your license's name?" to `Zavage Software Freeware License 1.0`.
|
||||
The listing's License link and each version's "Source code released under …"
|
||||
line show this name, not the text's first line.
|
||||
3. Paste `LICENSE.txt` unchanged, and leave the End-User License Agreement box
|
||||
unticked.
|
||||
4. Save. Saving edits the shared record, so 1.0, 1.2 and 1.3 all show the new
|
||||
text, and the next upload inherits it.
|
||||
5. Check every version:
|
||||
|
||||
```sh
|
||||
curl -s https://addons.mozilla.org/api/v5/addons/addon/search-with-chatgpt/versions/ \
|
||||
| jq '.results[] | {version, license: .license.name}'
|
||||
```
|
||||
|
||||
AMO turns the email address into a link to its own home page. It looks odd but
|
||||
does no harm.
|
||||
|
||||
## Outside the license
|
||||
|
||||
- **Register the copyright** with the US Copyright Office (Standard Application,
|
||||
US$65), and exclude AI-generated material that is more than trivial.
|
||||
Registration is needed to sue and to keep a takedown in place after a
|
||||
counter-notice. Statutory damages and fees also depend on registering within
|
||||
three months of first publication.
|
||||
- 1.2 and 1.3 reached AMO on 18 and 19 October 2026, so their window runs to
|
||||
about 18 January 2027.
|
||||
- The 1.1 code was pushed to the public Gitea in July and August 2026. If
|
||||
those pushes count as publication, its window closes around 29 October to
|
||||
1 November 2026. The old license's copy ban suggests they didn't, but that
|
||||
reading is low confidence.
|
||||
- So file by late October 2026 to be safe, and have an IP attorney confirm.
|
||||
- **Replace the Chrome repository's `LICENSE.txt`.** It still carries the old
|
||||
EULA and the public@zavage.net contact.
|
||||
- **Report clones** through Mozilla's infringement form or
|
||||
dmcanotice@mozilla.com. On AMO, Mozilla's own fork rule is a second route. Use
|
||||
the Chrome Web Store's and Microsoft's DMCA forms for those stores.
|
||||
- **Don't mirror the repository publicly on GitHub.** GitHub's terms let any
|
||||
GitHub user fork a public repository there.
|
||||
+2
-1
@@ -4,7 +4,7 @@
|
||||
"manifest_version": 2,
|
||||
"author": "Mathew Guest <mat@zavage.net>",
|
||||
"homepage_url": "https://zavage-software.com/portfolio/search-with-ChatGPT",
|
||||
"version": "1.3",
|
||||
"version": "1.2",
|
||||
"icons": {
|
||||
"16": "icons/powered-by-openai-icon-16x16.png",
|
||||
"32": "icons/powered-by-openai-icon-32x32.png",
|
||||
@@ -59,6 +59,7 @@
|
||||
"name": "ChatGPT",
|
||||
"search_url": "https://chatgpt.com?q={searchTerms}",
|
||||
"keyword": "gpt",
|
||||
"favicon_url": "https://zavage-software.com/static/ChatGPT-favicon.png",
|
||||
"is_default": false,
|
||||
"encoding": "UTF-8"
|
||||
}
|
||||
|
||||
+15
-349
@@ -5,11 +5,6 @@
|
||||
--border: #8f8f9d;
|
||||
--field-bg: #ffffff;
|
||||
--accent: #0060df;
|
||||
/* Backdrop for the drawings and inline code. */
|
||||
--surface: #f0f0f4;
|
||||
--tooltip-bg: #15141a;
|
||||
--tooltip-fg: #fbfbfe;
|
||||
--danger: #d70022;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -20,10 +15,6 @@
|
||||
--border: #8f8f9d;
|
||||
--field-bg: #2b2a33;
|
||||
--accent: #0df;
|
||||
--surface: #1c1b22;
|
||||
--tooltip-bg: #52525e;
|
||||
--tooltip-fg: #fbfbfe;
|
||||
--danger: #ff848b;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,270 +30,7 @@ body {
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.ways {
|
||||
margin: 0 0 0.75rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ways > li {
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.inline-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: -3px;
|
||||
}
|
||||
|
||||
/* Simplified drawings of Firefox's address bar and right-click menu. They are
|
||||
not screenshots, so they follow the theme and never go out of date. */
|
||||
.drawing {
|
||||
max-width: 24rem;
|
||||
margin: 0.45rem auto 0;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
font-size: 0.85rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.drawing img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.drawing-urlbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
background: var(--field-bg);
|
||||
outline: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.drawing-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--fg) 10%, var(--field-bg));
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drawing-typed {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* The text cursor. */
|
||||
.drawing-typed::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 1.1em;
|
||||
margin-inline-start: 1px;
|
||||
background: currentColor;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.drawing-page {
|
||||
margin: 0 0 0.3rem;
|
||||
}
|
||||
|
||||
.drawing mark {
|
||||
color: inherit;
|
||||
background: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
}
|
||||
|
||||
.drawing-toolbar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.drawing-address {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
background: var(--field-bg);
|
||||
color: var(--fg-dim);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.drawing-button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.drawing-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.3;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.drawing-button-hover {
|
||||
background: color-mix(in srgb, var(--fg) 14%, transparent);
|
||||
}
|
||||
|
||||
/* The mouse pointer resting on the hovered button. */
|
||||
.drawing-button svg.drawing-pointer {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 17px;
|
||||
width: 12px;
|
||||
height: 18px;
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.drawing-tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.65rem);
|
||||
right: 0;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: var(--tooltip-bg);
|
||||
color: var(--tooltip-fg);
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
/* Top padding keeps the heading clear of the tooltip at narrow widths. */
|
||||
.drawing-content {
|
||||
margin: 0.6rem 0 0;
|
||||
padding: 1.9rem 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
background: var(--field-bg);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.drawing-menu {
|
||||
width: 11rem;
|
||||
/* Opens at the pointer, just under the selection. */
|
||||
margin-inline-start: 3.5rem;
|
||||
padding: 0.25rem 0;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
|
||||
border-radius: 6px;
|
||||
background: var(--field-bg);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 0.2);
|
||||
}
|
||||
|
||||
.drawing-menu > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
/* Line the plain items up with the text of the item that has an icon. */
|
||||
padding: 0.2rem 0.75rem 0.2rem calc(0.75rem + 16px + 0.45rem);
|
||||
}
|
||||
|
||||
.drawing-menu > .drawing-menu-active {
|
||||
padding-inline-start: 0.75rem;
|
||||
background: color-mix(in srgb, var(--accent) 20%, var(--field-bg));
|
||||
}
|
||||
|
||||
.drawing-menu hr {
|
||||
margin: 0.25rem 0;
|
||||
border: 0;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
||||
}
|
||||
|
||||
/* The Advanced submenu, open beside the highlighted Ask ChatGPT item. */
|
||||
.drawing-advanced {
|
||||
margin-bottom: 1.15rem;
|
||||
}
|
||||
|
||||
.drawing-menus {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.drawing-menus > .drawing-menu {
|
||||
flex: none;
|
||||
width: 10rem;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.drawing-menus > .drawing-submenu {
|
||||
width: 8.5rem;
|
||||
/* Lines its first item up with Ask ChatGPT, overlapping the menu edge. */
|
||||
margin-top: 3.75rem;
|
||||
margin-inline-start: -0.3rem;
|
||||
}
|
||||
|
||||
/* Submenu items have no icon, so the highlighted one keeps the icon gap. */
|
||||
.drawing-submenu > .drawing-menu-active {
|
||||
padding-inline-start: calc(0.75rem + 16px + 0.45rem);
|
||||
}
|
||||
|
||||
.drawing-submenu-arrow {
|
||||
margin-inline-start: auto;
|
||||
font-size: 1.2em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* The shortcut's keys, held down. shortcut.js fills these in. */
|
||||
.drawing-keycaps {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 0;
|
||||
color: var(--fg-dim);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.drawing-key {
|
||||
min-width: 1.4rem;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 55%, var(--border));
|
||||
border-bottom-width: 3px;
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--field-bg));
|
||||
color: var(--fg);
|
||||
text-align: center;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.hint.intro-hint {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.usage,
|
||||
.settings {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.hint.settings-scope {
|
||||
margin: 0 0 1rem;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -337,8 +65,7 @@ select {
|
||||
|
||||
input[type="text"]:focus-visible,
|
||||
select:focus-visible,
|
||||
button:focus-visible,
|
||||
summary:focus-visible {
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
@@ -349,30 +76,18 @@ summary:focus-visible {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
details[open] > summary {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.more-options {
|
||||
margin-bottom: 1.15rem;
|
||||
}
|
||||
|
||||
.summary-detail {
|
||||
.tag {
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
color: var(--fg-dim);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
padding: 0 0.3rem;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
|
||||
/* Reversed so the button stays at the right edge and "Saved" appears beside
|
||||
it without moving it. */
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
@@ -389,69 +104,35 @@ button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Restore defaults discards every customised setting at once. */
|
||||
button.danger {
|
||||
color: var(--danger);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
button.danger:hover {
|
||||
color: var(--bg);
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
#status {
|
||||
color: var(--fg-dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin-top: 1.25rem;
|
||||
margin-top: 1.75rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--fg-dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.note summary {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 0.25em;
|
||||
border-radius: 3px;
|
||||
background: var(--surface);
|
||||
code,
|
||||
kbd {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Written as <Ctrl>+L, so the keys read as words rather than code. */
|
||||
kbd {
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.usage h2,
|
||||
.settings h2 {
|
||||
.note h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--fg);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.note p,
|
||||
.note ol {
|
||||
.note p {
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.note ol {
|
||||
padding-inline-start: 1.25rem;
|
||||
}
|
||||
|
||||
.note li {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.note code {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -459,9 +140,8 @@ kbd {
|
||||
.project-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem 0.65rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
@@ -487,23 +167,9 @@ a:focus-visible {
|
||||
/* The inline about:addons surface can be very narrow. */
|
||||
@media (max-width: 26rem) {
|
||||
.actions {
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.drawing-menu {
|
||||
margin-inline-start: 1rem;
|
||||
}
|
||||
|
||||
/* Stack the links rather than wrap one onto a line that starts with "|". */
|
||||
.project-links {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link-separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
+57
-158
@@ -7,120 +7,37 @@
|
||||
<link rel="stylesheet" href="options.css" />
|
||||
|
||||
<p class="intro">
|
||||
This extension adds <a href="https://chatgpt.com/" target="_blank" rel="noopener noreferrer">ChatGPT</a> as a Firefox search engine.
|
||||
This extension adds <a href="https://chatgpt.com/" target="_blank" rel="noopener noreferrer">ChatGPT</a> as a Firefox search engine. Type
|
||||
<code>gpt your question</code> in the address bar, select text on a webpage and
|
||||
choose <strong>Ask ChatGPT</strong> from the right-click menu.<span id="shortcut-intro"></span>
|
||||
</p>
|
||||
|
||||
<section class="usage" aria-labelledby="usage-title">
|
||||
<h2 id="usage-title">Usage instructions</h2>
|
||||
|
||||
<!-- Hotkeys are written <Ctrl>+L: named keys in angle brackets, letters plain.
|
||||
shortcut.js rewrites each [data-shortcut] for the user's platform. The
|
||||
drawings are decorative; the text beside them carries the instructions. -->
|
||||
<ul class="ways">
|
||||
<li>
|
||||
<strong>Address bar:</strong> press <kbd data-shortcut="Ctrl+L"><Ctrl>+L</kbd>,
|
||||
type <code>gpt</code> and a space, then your question, and press
|
||||
<kbd data-shortcut="Enter"><Enter></kbd>.
|
||||
<div class="drawing" aria-hidden="true">
|
||||
<div class="drawing-urlbar">
|
||||
<span class="drawing-chip"><img src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />ChatGPT</span>
|
||||
<span class="drawing-typed">how do magnets work?</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Right-click:</strong> select text on a webpage, right-click it, and
|
||||
choose <strong>Ask ChatGPT</strong>.
|
||||
<div class="drawing" aria-hidden="true">
|
||||
<p class="drawing-page">Fridge <mark>magnets stick</mark> to steel because…</p>
|
||||
<div class="drawing-menu">
|
||||
<div>Copy</div>
|
||||
<div>Select All</div>
|
||||
<hr />
|
||||
<div class="drawing-menu-active"><img src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />Ask ChatGPT</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li id="shortcut-intro" hidden>
|
||||
<strong>Keyboard:</strong> select text and press <kbd id="shortcut-intro-key"></kbd>.
|
||||
With nothing selected, it opens a blank chat.
|
||||
<div class="drawing" aria-hidden="true">
|
||||
<div class="drawing-keycaps" id="shortcut-keycaps"></div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Toolbar:</strong> select text and click the
|
||||
<img class="inline-icon" src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />
|
||||
<strong>Ask ChatGPT</strong> button. With nothing selected, it opens a
|
||||
blank chat. Can’t see the button? Look in the puzzle-piece Extensions menu.
|
||||
<div class="drawing" aria-hidden="true">
|
||||
<div class="drawing-toolbar">
|
||||
<span class="drawing-address">example.com</span>
|
||||
<span class="drawing-button">
|
||||
<svg viewBox="0 0 16 16"><path d="M2 5.5h3a2 2 0 1 1 3.2 0h3.3v2.8a2 2 0 1 1 0 3.2v3H2z" /></svg>
|
||||
</span>
|
||||
<span class="drawing-button drawing-button-hover">
|
||||
<img src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />
|
||||
<svg class="drawing-pointer" viewBox="0 0 12 18"><path d="M1 1v14.5l3.6-3.4 2.6 5.6 2.2-1-2.6-5.5H11z" /></svg>
|
||||
</span>
|
||||
<span class="drawing-button">
|
||||
<svg viewBox="0 0 16 16"><path d="M3 4.5h10M3 8h10M3 11.5h10" /></svg>
|
||||
</span>
|
||||
<span class="drawing-tooltip">Ask ChatGPT</span>
|
||||
</div>
|
||||
<p class="drawing-content"><mark>Why fridge magnets stick</mark></p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="hint intro-hint">
|
||||
ChatGPT opens with your text filled in. If it isn’t sent automatically, press
|
||||
<kbd data-shortcut="Enter"><Enter></kbd> or click Send. ChatGPT may ask you to sign in first.
|
||||
<p class="intro">
|
||||
Click the <strong>Ask ChatGPT</strong> toolbar button to ask about selected
|
||||
text, or open a blank chat when nothing is selected. The button works in
|
||||
every context-menu mode and follows your settings below.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="settings" aria-labelledby="settings-title">
|
||||
<h2 id="settings-title">Settings</h2>
|
||||
<p class="hint settings-scope">
|
||||
These apply to the right-click menu, toolbar button, and keyboard shortcut.
|
||||
Typing <code>gpt</code> in the address bar isn’t affected.
|
||||
<p class="intro">
|
||||
ChatGPT may require you to sign in. If your prompt is not sent automatically,
|
||||
press <strong>Enter</strong> or click <strong>Send</strong> in ChatGPT.
|
||||
</p>
|
||||
|
||||
<form id="settings" autocomplete="off">
|
||||
<div class="field">
|
||||
<label for="contextMenuMode">Right-click menu</label>
|
||||
<label for="contextMenuMode">Context menu</label>
|
||||
<select id="contextMenuMode">
|
||||
<option value="off">Off</option>
|
||||
<option value="simple">Simple</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
</select>
|
||||
<p class="hint">
|
||||
Simple shows Ask ChatGPT. Advanced adds Explain, Summarize, Debug code,
|
||||
and more. With Off, the toolbar button and keyboard shortcut still work.
|
||||
</p>
|
||||
<p class="hint">Simple asks about selected text. Advanced adds Explain, Summarize, Debug code, and link/page actions.</p>
|
||||
<p class="hint" id="shortcut-hint">The keyboard shortcut always runs your default action.</p>
|
||||
</div>
|
||||
|
||||
<div id="advanced-settings" class="dependent-settings" hidden>
|
||||
<!-- Shown only with Advanced, so the drawing previews what it adds. -->
|
||||
<div class="drawing drawing-advanced" aria-hidden="true">
|
||||
<div class="drawing-menus">
|
||||
<div class="drawing-menu">
|
||||
<div>Copy</div>
|
||||
<div>Select All</div>
|
||||
<hr />
|
||||
<div class="drawing-menu-active"><img src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />Ask ChatGPT<span class="drawing-submenu-arrow">›</span></div>
|
||||
</div>
|
||||
<div class="drawing-menu drawing-submenu">
|
||||
<div>Ask normally</div>
|
||||
<hr />
|
||||
<div class="drawing-menu-active">Explain</div>
|
||||
<div>Summarize</div>
|
||||
<div>Debug code</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field checkbox">
|
||||
<label><input type="checkbox" id="showTranslate"> Show Translate in the Advanced menu</label>
|
||||
<label><input type="checkbox" id="showTranslate"> Show Translate in advanced menu</label>
|
||||
</div>
|
||||
<div id="translation-settings" hidden>
|
||||
<div class="field">
|
||||
@@ -139,8 +56,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field checkbox">
|
||||
<label><input type="checkbox" id="temporaryChat" /> Request a temporary chat</label>
|
||||
<p class="hint">
|
||||
ChatGPT may not honour this. Check ChatGPT's own indicator before sending
|
||||
anything sensitive.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field checkbox">
|
||||
<label><input type="checkbox" id="webSearch" /> Ask ChatGPT to use web search (hint only)</label>
|
||||
<p class="hint">A hint to ChatGPT's interface, not a guarantee.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="openIn">Open ChatGPT in</label>
|
||||
<label for="openIn">Open in</label>
|
||||
<select id="openIn">
|
||||
<option value="new-tab">A new tab</option>
|
||||
<option value="background-tab">A new background tab</option>
|
||||
@@ -148,77 +78,46 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Opens on load when one of these differs from its default (options.js),
|
||||
so a customised setting never looks lost. -->
|
||||
<details id="more-options" class="more-options">
|
||||
<summary>More options: <span class="summary-detail">prompt template, temporary chat, web search</span></summary>
|
||||
|
||||
<div class="field">
|
||||
<label for="promptTemplate">Prompt template</label>
|
||||
<input type="text" id="promptTemplate" spellcheck="false" maxlength="1000" />
|
||||
<p class="hint">
|
||||
<code>{query}</code> is replaced with your selected text, as in
|
||||
<code>Explain simply: {query}</code>. Used by Ask ChatGPT, Ask normally,
|
||||
the toolbar button, and the keyboard shortcut; other Advanced actions
|
||||
such as Summarize use their own prompts. Up to 1,000 characters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field checkbox">
|
||||
<label><input type="checkbox" id="temporaryChat" /> Request a temporary chat</label>
|
||||
<p class="hint">
|
||||
ChatGPT may not honour this. Check ChatGPT's own indicator before sending
|
||||
anything sensitive.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field checkbox">
|
||||
<label><input type="checkbox" id="webSearch" /> Ask ChatGPT to use web search (hint only)</label>
|
||||
<p class="hint">A hint to ChatGPT's interface, not a guarantee.</p>
|
||||
</div>
|
||||
</details>
|
||||
<div class="field">
|
||||
<label for="promptTemplate">Prompt template</label>
|
||||
<input type="text" id="promptTemplate" spellcheck="false" maxlength="1000" />
|
||||
<p class="hint"><code>{query}</code> is replaced with your selected text. Used by Ask normally, Simple mode, the toolbar button, and the keyboard shortcut. Other advanced actions use their own prompts. Up to 1,000 characters.</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" id="restore" class="danger">Restore defaults</button>
|
||||
<button type="button" id="restore">Restore defaults</button>
|
||||
<span id="status" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="note" aria-labelledby="search-settings-title">
|
||||
<h2 id="search-settings-title">Search keyword and default engine</h2>
|
||||
<p>
|
||||
Open Firefox’s <strong>Settings → Search</strong>, or paste
|
||||
<code>about:preferences#search</code> into the address bar.
|
||||
Under <strong>Search Shortcuts</strong>, edit ChatGPT’s keyword to change
|
||||
<code>gpt</code>. Under <strong>Default Search Engine</strong>, choose
|
||||
ChatGPT to use it for address-bar searches without a prefix. Choose your
|
||||
previous engine there to switch back.
|
||||
<a href="https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox" target="_blank" rel="noopener noreferrer">Firefox search settings guide</a>.
|
||||
</p>
|
||||
<p>
|
||||
The settings above apply to the right-click menu, toolbar button, and keyboard shortcut;
|
||||
address-bar searches use Firefox’s search engine settings.
|
||||
Restore defaults only resets extension settings, including the prompt
|
||||
template; it does not change Firefox’s search engine or keyboard shortcut.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<details class="note">
|
||||
<summary>Use your own keyword or make ChatGPT your default search engine</summary>
|
||||
<ol>
|
||||
<li>
|
||||
Open Firefox’s <strong>Settings → Search</strong>, or paste
|
||||
<code>about:preferences#search</code> into the address bar.
|
||||
</li>
|
||||
<li>
|
||||
Under <strong>Search Shortcuts</strong>, edit ChatGPT’s keyword to add
|
||||
your own. <code>gpt</code> keeps working too.
|
||||
</li>
|
||||
<li>
|
||||
Under <strong>Default Search Engine</strong>, choose ChatGPT to use it
|
||||
without typing <code>gpt</code>. Choose your previous engine there to
|
||||
switch back.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
These are Firefox settings, so <strong>Restore defaults</strong> doesn’t
|
||||
change them.
|
||||
<a href="https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox" target="_blank" rel="noopener noreferrer">Firefox search settings guide</a>
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details class="note">
|
||||
<summary>Change the keyboard shortcut</summary>
|
||||
<section class="note" aria-labelledby="shortcut-title">
|
||||
<h2 id="shortcut-title">Keyboard shortcut</h2>
|
||||
<p id="shortcut-current" role="status" aria-live="polite">Checking keyboard shortcut…</p>
|
||||
<button type="button" id="change-shortcut" hidden>Change keyboard shortcut</button>
|
||||
<p class="hint">
|
||||
Or open Add-ons, click the gear menu, and choose
|
||||
<em>Manage Extension Shortcuts</em>. <strong>Restore defaults</strong>
|
||||
doesn’t change your shortcut.
|
||||
You can also open Add-ons, then the gear menu →
|
||||
<em>Manage Extension Shortcuts</em> to change or remove the shortcut.
|
||||
</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<nav class="project-links" aria-label="Project links">
|
||||
<a href="https://zavage-software.com/portfolio/search-with-ChatGPT" target="_blank" rel="noopener noreferrer">Project website</a>
|
||||
|
||||
+4
-31
@@ -1,27 +1,13 @@
|
||||
import { DEFAULT_SETTINGS } from "../lib/defaults.js";
|
||||
import { load, save } from "../lib/settings.js";
|
||||
import { initializeMenuSettings, MENU_SETTING_KEYS } from "./menu-settings.js";
|
||||
import { initializeShortcut, renderStaticShortcuts } from "./shortcut.js";
|
||||
import { initializeShortcut } from "./shortcut.js";
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 400;
|
||||
const STATUS_CLEAR_MS = 1500;
|
||||
|
||||
const field = (id) => document.getElementById(id);
|
||||
|
||||
// The settings inside the collapsed "More options" section, read from the page
|
||||
// so moving a setting there needs no change here.
|
||||
const MORE_OPTION_KEYS = [...field("more-options").querySelectorAll("[id]")]
|
||||
.map(({ id }) => id)
|
||||
.filter((id) => Object.hasOwn(DEFAULT_SETTINGS, id));
|
||||
|
||||
// A customised setting should never look lost inside a closed section. This
|
||||
// only ever opens it, so it never undoes the user collapsing it.
|
||||
function revealCustomised(settings, keys = MORE_OPTION_KEYS) {
|
||||
if (keys.some((key) => settings[key] !== DEFAULT_SETTINGS[key])) {
|
||||
field("more-options").open = true;
|
||||
}
|
||||
}
|
||||
|
||||
let saveTimer = null;
|
||||
let statusTimer = null;
|
||||
|
||||
@@ -80,14 +66,8 @@ function scheduleSave() {
|
||||
}
|
||||
|
||||
// Module scripts are deferred, so the DOM is already parsed here.
|
||||
const [{ os }, initial] = await Promise.all([
|
||||
browser.runtime.getPlatformInfo().catch(() => ({})),
|
||||
load(),
|
||||
]);
|
||||
renderStaticShortcuts(document, os);
|
||||
void initializeShortcut({ commands: browser.commands, document, window, os });
|
||||
render(initial);
|
||||
revealCustomised(initial);
|
||||
void initializeShortcut({ commands: browser.commands, document, window });
|
||||
render(await load());
|
||||
|
||||
for (const id of Object.keys(DEFAULT_SETTINGS)) {
|
||||
if (MENU_SETTING_KEYS.includes(id)) continue;
|
||||
@@ -113,12 +93,5 @@ browser.storage.onChanged.addListener(async (changes, area) => {
|
||||
if (area !== "sync") {
|
||||
return;
|
||||
}
|
||||
const settings = await load();
|
||||
render(settings);
|
||||
// Only keys whose value really changed, such as a sync from another device:
|
||||
// this page saves every key at once, so checking them all would re-open the
|
||||
// section whenever any other setting is edited.
|
||||
revealCustomised(settings, MORE_OPTION_KEYS.filter(
|
||||
(key) => key in changes && changes[key].newValue !== changes[key].oldValue,
|
||||
));
|
||||
render(await load());
|
||||
});
|
||||
|
||||
+8
-42
@@ -1,52 +1,18 @@
|
||||
// Firefox stores Mac shortcuts with Command, MacCtrl, and Alt, and reads a
|
||||
// manifest's Ctrl as Command there (ShortcutUtils' chromeModifierKeyMap).
|
||||
const MAC_KEY_NAMES = { Ctrl: "Cmd", Command: "Cmd", MacCtrl: "Ctrl", Alt: "Option", Enter: "Return" };
|
||||
const OTHER_KEY_NAMES = { Command: "Ctrl", MacCtrl: "Ctrl" };
|
||||
|
||||
// The keys of a Firefox shortcut such as "Alt+Shift+G", named for the platform.
|
||||
export function shortcutKeys(shortcut, os) {
|
||||
const names = os === "mac" ? MAC_KEY_NAMES : OTHER_KEY_NAMES;
|
||||
return shortcut.split("+").map((key) => names[key] ?? key);
|
||||
}
|
||||
|
||||
// Writes a shortcut the way the page does: named keys in angle brackets and
|
||||
// single characters plain, "<Alt>+<Shift>+G".
|
||||
export function formatShortcut(shortcut, os) {
|
||||
return shortcutKeys(shortcut, os)
|
||||
.map((name) => (name.length > 1 ? `<${name}>` : name))
|
||||
.join("+");
|
||||
}
|
||||
|
||||
// The page's static hotkeys are written for Windows and Linux.
|
||||
export function renderStaticShortcuts(document, os) {
|
||||
for (const element of document.querySelectorAll("[data-shortcut]")) {
|
||||
element.textContent = formatShortcut(element.dataset.shortcut, os);
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox owns shortcut preferences; never copy them into extension storage.
|
||||
export async function initializeShortcut({ commands, document, window, os }) {
|
||||
export async function initializeShortcut({ commands, document, window }) {
|
||||
const field = (id) => document.getElementById(id);
|
||||
let refreshId = 0;
|
||||
|
||||
// One key cap per key for the drawing, joined by plain "+" text.
|
||||
function keycaps(shortcut) {
|
||||
return shortcutKeys(shortcut, os).flatMap((name, index) => {
|
||||
const key = document.createElement("kbd");
|
||||
key.className = "drawing-key";
|
||||
key.textContent = name;
|
||||
return index ? ["+", key] : [key];
|
||||
});
|
||||
}
|
||||
|
||||
function render(shortcut) {
|
||||
const keys = shortcut ? formatShortcut(shortcut, os) : "";
|
||||
field("shortcut-intro").hidden = !keys;
|
||||
field("shortcut-intro-key").textContent = keys;
|
||||
field("shortcut-keycaps").replaceChildren(...(keys ? keycaps(shortcut) : []));
|
||||
field("shortcut-intro").textContent = shortcut
|
||||
? ` Or press ${shortcut} to use your selection. With nothing selected, the shortcut opens a blank chat.`
|
||||
: "";
|
||||
field("shortcut-hint").textContent = shortcut
|
||||
? `${shortcut} runs your default action even when the context menu is off.`
|
||||
: "The keyboard shortcut runs your default action independently of the context menu.";
|
||||
field("shortcut-current").textContent = shortcut === null
|
||||
? "Could not read the keyboard shortcut. Check Manage Extension Shortcuts in Add-ons."
|
||||
: keys ? `Current shortcut: ${keys}` : "No keyboard shortcut assigned.";
|
||||
: shortcut ? `Current shortcut: ${shortcut}` : "No keyboard shortcut assigned.";
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
|
||||
Generated
+2
-3
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||
"version": "1.3.0",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||
"version": "1.3.0",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"version": "1.2.0",
|
||||
"devDependencies": {
|
||||
"web-ext": "10.5.0"
|
||||
},
|
||||
|
||||
+1
-2
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||
"version": "1.3.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Development tooling for the Search with ChatGPT Firefox extension.",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
|
||||
+12
-70
@@ -1,87 +1,32 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { formatShortcut, initializeShortcut, renderStaticShortcuts } from "../options/shortcut.js";
|
||||
import { initializeShortcut } from "../options/shortcut.js";
|
||||
|
||||
function setup(commands, os) {
|
||||
function setup(commands) {
|
||||
const elements = Object.fromEntries([
|
||||
"shortcut-intro", "shortcut-intro-key", "shortcut-keycaps", "shortcut-current", "change-shortcut",
|
||||
].map((id) => [id, Object.assign(new EventTarget(), {
|
||||
textContent: "",
|
||||
hidden: true,
|
||||
children: [],
|
||||
replaceChildren(...children) { this.children = children; },
|
||||
})]));
|
||||
"shortcut-intro", "shortcut-hint", "shortcut-current", "change-shortcut",
|
||||
].map((id) => [id, Object.assign(new EventTarget(), { textContent: "", hidden: true })]));
|
||||
const document = Object.assign(new EventTarget(), {
|
||||
visibilityState: "visible",
|
||||
getElementById: (id) => elements[id],
|
||||
createElement: (tagName) => ({ tagName, className: "", textContent: "" }),
|
||||
});
|
||||
const window = new EventTarget();
|
||||
const ready = initializeShortcut({ commands, document, window, os });
|
||||
const ready = initializeShortcut({ commands, document, window });
|
||||
return { elements, document, window, ready };
|
||||
}
|
||||
|
||||
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
||||
const command = (shortcut) => ({ name: "ask-chatgpt", description: "Ask ChatGPT", shortcut });
|
||||
// The drawing's key caps and the "+" text between them, in order.
|
||||
const drawn = (element) => element.children.map((child) => child.textContent ?? child);
|
||||
|
||||
test("shows the configured command rather than another command or the default", async () => {
|
||||
const { elements, ready } = setup({ getAll: async () => [
|
||||
{ name: "other", description: "Other", shortcut: "Alt+O" }, command("Ctrl+Shift+Y"),
|
||||
] });
|
||||
await ready;
|
||||
assert.equal(elements["shortcut-intro"].hidden, false);
|
||||
assert.equal(elements["shortcut-intro-key"].textContent, "<Ctrl>+<Shift>+Y");
|
||||
assert.equal(elements["shortcut-current"].textContent, "Current shortcut: <Ctrl>+<Shift>+Y");
|
||||
assert.deepEqual(drawn(elements["shortcut-keycaps"]), ["Ctrl", "+", "Shift", "+", "Y"]);
|
||||
assert.ok(elements["shortcut-keycaps"].children
|
||||
.filter((child) => typeof child !== "string")
|
||||
.every(({ tagName, className }) => tagName === "kbd" && className === "drawing-key"));
|
||||
});
|
||||
|
||||
test("writes keys the way the page does, with Mac names on a Mac", () => {
|
||||
assert.equal(formatShortcut("Alt+Shift+G", "linux"), "<Alt>+<Shift>+G");
|
||||
assert.equal(formatShortcut("Ctrl+L", "win"), "<Ctrl>+L");
|
||||
assert.equal(formatShortcut("Enter", undefined), "<Enter>");
|
||||
assert.equal(formatShortcut("Ctrl+Shift+F5", "win"), "<Ctrl>+<Shift>+<F5>");
|
||||
// Firefox names punctuation and media keys rather than using the character.
|
||||
assert.equal(formatShortcut("Ctrl+Shift+Comma", "win"), "<Ctrl>+<Shift>+<Comma>");
|
||||
assert.equal(formatShortcut("MediaPlayPause", "linux"), "<MediaPlayPause>");
|
||||
assert.equal(formatShortcut("Alt+Shift+G", "mac"), "<Option>+<Shift>+G");
|
||||
// Firefox reads a manifest's Ctrl as Command on a Mac, and stores shortcuts
|
||||
// the user sets there with Command and MacCtrl.
|
||||
assert.equal(formatShortcut("Ctrl+L", "mac"), "<Cmd>+L");
|
||||
assert.equal(formatShortcut("Command+Shift+Y", "mac"), "<Cmd>+<Shift>+Y");
|
||||
assert.equal(formatShortcut("MacCtrl+Alt+Y", "mac"), "<Ctrl>+<Option>+Y");
|
||||
assert.equal(formatShortcut("Enter", "mac"), "<Return>");
|
||||
});
|
||||
|
||||
test("a Mac shows its own key names for the configured shortcut", async () => {
|
||||
const { elements, ready } = setup({ getAll: async () => [command("Alt+Shift+G")] }, "mac");
|
||||
await ready;
|
||||
assert.equal(elements["shortcut-intro-key"].textContent, "<Option>+<Shift>+G");
|
||||
assert.deepEqual(drawn(elements["shortcut-keycaps"]), ["Option", "+", "Shift", "+", "G"]);
|
||||
});
|
||||
|
||||
test("the page's written hotkeys match the formatter and are rewritten per platform", () => {
|
||||
const html = readFileSync(new URL("../options/options.html", import.meta.url), "utf8");
|
||||
const written = [...html.matchAll(/data-shortcut="([^"]+)">([^<]*)</g)];
|
||||
assert.ok(written.length >= 2);
|
||||
// Every hotkey must match the pattern above, or this test would skip it.
|
||||
assert.equal(written.length, html.split("data-shortcut=").length - 1);
|
||||
for (const [, shortcut, text] of written) {
|
||||
assert.equal(text.replaceAll("<", "<").replaceAll(">", ">"), formatShortcut(shortcut, "linux"));
|
||||
for (const id of ["shortcut-intro", "shortcut-hint", "shortcut-current"]) {
|
||||
assert.match(elements[id].textContent, /Ctrl\+Shift\+Y/);
|
||||
assert.doesNotMatch(elements[id].textContent, /Alt\+Shift\+G|Alt\+O/);
|
||||
}
|
||||
|
||||
const elements = written.map(([, shortcut]) => ({ dataset: { shortcut }, textContent: "" }));
|
||||
renderStaticShortcuts({ querySelectorAll: () => elements }, "mac");
|
||||
assert.deepEqual(
|
||||
elements.map(({ textContent }) => textContent),
|
||||
written.map(([, shortcut]) => formatShortcut(shortcut, "mac")),
|
||||
);
|
||||
assert.ok(elements.some(({ textContent }) => textContent === "<Cmd>+L"));
|
||||
});
|
||||
|
||||
test("updates an open page when the shortcut is removed", async () => {
|
||||
@@ -96,9 +41,7 @@ test("updates an open page when the shortcut is removed", async () => {
|
||||
changed({ name: "ask-chatgpt", newShortcut: "", oldShortcut: "Alt+Shift+G" });
|
||||
await flush();
|
||||
assert.match(elements["shortcut-current"].textContent, /No keyboard shortcut assigned/);
|
||||
assert.equal(elements["shortcut-intro"].hidden, true);
|
||||
assert.equal(elements["shortcut-intro-key"].textContent, "");
|
||||
assert.deepEqual(elements["shortcut-keycaps"].children, []);
|
||||
assert.doesNotMatch(elements["shortcut-intro"].textContent, /press|Alt\+Shift\+G/i);
|
||||
});
|
||||
|
||||
test("refreshes on focus and visibility without the change event API", async () => {
|
||||
@@ -108,11 +51,11 @@ test("refreshes on focus and visibility without the change event API", async ()
|
||||
shortcut = "Ctrl+Shift+Y";
|
||||
window.dispatchEvent(new Event("focus"));
|
||||
await flush();
|
||||
assert.match(elements["shortcut-current"].textContent, /<Ctrl>\+<Shift>\+Y/);
|
||||
assert.match(elements["shortcut-current"].textContent, /Ctrl\+Shift\+Y/);
|
||||
shortcut = "Alt+J";
|
||||
document.dispatchEvent(new Event("visibilitychange"));
|
||||
await flush();
|
||||
assert.match(elements["shortcut-current"].textContent, /<Alt>\+J/);
|
||||
assert.match(elements["shortcut-current"].textContent, /Alt\+J/);
|
||||
});
|
||||
|
||||
test("missing commands and failed reads never claim the default is assigned", async () => {
|
||||
@@ -121,8 +64,7 @@ test("missing commands and failed reads never claim the default is assigned", as
|
||||
}]) {
|
||||
const { elements, ready } = setup(commands);
|
||||
await ready;
|
||||
assert.doesNotMatch(elements["shortcut-current"].textContent, /<Alt>\+<Shift>\+G|Alt\+Shift\+G/);
|
||||
assert.equal(elements["shortcut-intro"].hidden, true);
|
||||
assert.doesNotMatch(elements["shortcut-current"].textContent, /Alt\+Shift\+G/);
|
||||
assert.equal(elements["change-shortcut"].hidden, true);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user