Compare 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,17 +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).
|
||||
|
||||
## [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
|
||||
|
||||
@@ -31,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.
|
||||
@@ -61,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.
|
||||
+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; }
|
||||
|
||||
+50
-151
@@ -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,31 +56,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="openIn">Open ChatGPT in</label>
|
||||
<select id="openIn">
|
||||
<option value="new-tab">A new tab</option>
|
||||
<option value="background-tab">A new background tab</option>
|
||||
<option value="new-window">A new window</option>
|
||||
</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">
|
||||
@@ -176,49 +68,56 @@
|
||||
<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="openIn">Open in</label>
|
||||
<select id="openIn">
|
||||
<option value="new-tab">A new tab</option>
|
||||
<option value="background-tab">A new background tab</option>
|
||||
<option value="new-window">A new window</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<details class="note">
|
||||
<summary>Use your own keyword or make ChatGPT your default search engine</summary>
|
||||
<ol>
|
||||
<li>
|
||||
<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.
|
||||
</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>
|
||||
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>
|
||||
</details>
|
||||
<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>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
-2
@@ -1,12 +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",
|
||||
"version": "1.2.0",
|
||||
"devDependencies": {
|
||||
"web-ext": "10.5.0"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
|
||||
+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