Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
46eccf6
Fix two shipped examples that told people to type the wrong keyword
ion05 Sep 3, 2026
b230ea4
Rewrite the README as the front door of a public repository
ion05 Sep 3, 2026
42451f5
Null-prototype every lookup table an argument can index
ion05 Sep 3, 2026
8c0b89f
Make the privacy policy exhaustive about where data is kept
ion05 Sep 3, 2026
cd432b4
Drop the internal handover doc and keep the facts worth keeping
ion05 Sep 3, 2026
6884d7e
Correct the changelog where it contradicted itself and the code
ion05 Sep 3, 2026
f48e2cc
Disclaim the Meta affiliation on the screen that claims it
ion05 Sep 3, 2026
a868dec
Finish the repository for a first-time visitor
ion05 Sep 3, 2026
6cf1fff
Fix three ways the Shortcuts page lost the user's place or its state
ion05 Sep 3, 2026
074c8a5
Route the exempt-keyword field through the one validation boundary
ion05 Sep 3, 2026
0460918
Split storage.ts into the two families it always had
ion05 Sep 4, 2026
c38fc76
Split dnr.ts along its three concerns
ion05 Sep 4, 2026
0f1056b
Split the browse route so its one rule can be checked by grep
ion05 Sep 4, 2026
6bc73cc
Give the browse view a DOM test suite, on jsdom
ion05 Sep 4, 2026
7ccb702
Delete 39 test cases that were covering something else's ground
ion05 Sep 4, 2026
3790bbb
Add Prettier and ESLint, and put lint first in the gate
ion05 Sep 4, 2026
3c89e3b
Sweep the dead exports and the comments that describe old approaches
ion05 Sep 4, 2026
d259c3a
Turn on noUncheckedIndexedAccess and answer what it asks
ion05 Sep 4, 2026
4e25a7b
Cut the test suite to the 150 cases that would catch a broken build
ion05 Sep 4, 2026
1ef2032
docs: refresh README and add UI screenshots
ion05 Sep 4, 2026
e673cc0
Remove the Web Store submission material
ion05 Sep 4, 2026
0217322
Drop the lead-in sentence above the README examples
ion05 Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
* text=auto eol=lf
*.png binary
*.woff2 binary

# Exported artboards, not hand-written source. Marking them keeps GitHub's
# language bar and its diffs about the code somebody actually maintains.
design/canvas/*.dc.html linguist-generated=true
7 changes: 4 additions & 3 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ body:
attributes:
label: Rule status
description: >-
The rule-status text at the top of the options page, copied verbatim
(e.g. "Intercepting 148 keywords · 2 exempted by you")
The rule-status text at the top of the options page, copied verbatim,
if any is shown. A healthy profile shows nothing there, which is a
fine answer.
validations:
required: true
required: false
- type: textarea
id: console
attributes:
Expand Down
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# GitHub Actions only, deliberately.
#
# The npm ecosystem is left out because this project's dependency policy is the
# point: four devDependencies, no runtime dependencies, and nothing from
# node_modules reaches the shipped extension. A weekly stream of npm bumps would
# be noise against a lockfile that is meant to move rarely and on purpose.
#
# The action pins are the opposite case. They are `@v4` major tags on somebody
# else's repository, they are the only third-party code that runs with access to
# this repository, and nobody notices when one goes stale.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
commit-message:
prefix: 'ci:'
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ permissions:

jobs:
check:
name: typecheck + test + build
name: lint + typecheck + test + build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -25,14 +25,19 @@ jobs:
cache: pnpm

- run: pnpm install --frozen-lockfile

# First, because it is the fastest signal: eslint and prettier --check
# together run in a couple of seconds, well under the typecheck.
- run: pnpm lint

- run: pnpm typecheck
- run: pnpm test
- run: pnpm build

# `pnpm build` runs scripts/gen-icons.mjs, so a change to the generator or
# to --accent repaints these. Committed PNGs that the generator no longer
# produces are a silent drift no other step can see.
- run: git diff --exit-code -- public/icons store
- run: git diff --exit-code -- public/icons

# The packer has no test, it writes a binary nothing else reads, so the
# only cheap guard is that it still runs over a real build. `release/` is
Expand Down
27 changes: 27 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Prettier already skips file types it has no parser for, so this lists only
# what it WOULD format and should not.

# Build output and packaging artefacts. Nothing here is edited by hand.
dist/
release/

# Machine-written, and pnpm owns the formatting of its own lockfile.
pnpm-lock.yaml

# The approved design bundle. AGENTS.md: change it through a design review, not
# in passing. `design/canvas/*.dc.html` are exported artboards (.gitattributes
# already marks them linguist-generated), and `design/tokens.css` is parsed as
# text by scripts/gen-icons.mjs, which throws if the accent declarations move.
# Its trailing contrast-ratio comments are aligned by hand and carry the audit.
design/

# The dispatch page's inline stylesheet is deliberately minified: this page's
# whole job is to redirect before it paints, so it fetches no font and loads no
# sheet, and the values are copied by hand from design/tokens.css rather than
# substituted at build time.
go.html

# Prose is hand-wrapped at about 100 columns and uses *emphasis*. Prettier
# rewrites that to _emphasis_ and reflows paragraphs, which is churn on text no
# formatter can improve. The wrap width is a review convention, not a build rule.
*.md
10 changes: 10 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "always",
"endOfLine": "lf"
}
207 changes: 158 additions & 49 deletions AGENTS.md

Large diffs are not rendered by default.

25 changes: 13 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `gr` (also `goodreads`): search books and reviews on Goodreads. It shipped
until v1.1.0 dropped the `media` category it was filed under; it is back, in
- `gr` (also `goodreads`): search books and reviews on Goodreads, filed under
Search.
- `track <number>` (also `pkg`): one keyword for any parcel. BunnyLol reads
the carrier (UPS, USPS, FedEx or DHL) off the shape of the number and opens
Expand Down Expand Up @@ -83,20 +82,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
it is about to open, and offers an Open button, which holds the focus, and
the escape search. The 1.2 second toast it replaces navigated on its own,
which is a delay rather than a confirmation.
- The rule-status pill says **Shortcuts active** instead of counting
keywords, and **Some keywords not intercepted** when coverage is partial.
The count moved every time a shortcut was switched on or off, and nobody
acted on it. The numbers that do matter, what you exempted and what Chrome
refused, are still on the line under it and on the Settings coverage line.
- `web_accessible_resources` is narrowed to `go.html`. `go.js` and `assets/*`
are same-origin subresources of an extension page and never needed an
entry. Listing them exposed them, and the shipped sourcemaps, to the search
engines.

### Removed
- The green *Shortcuts active* pill. The rule-status pill in the topbar now
appears only when there is something to act on: partial coverage, a failed
sync, or interception switched off. A healthy profile shows nothing.
- The always-on rule-status pill. It now appears only when there is something
to act on: partial coverage, a failed sync, or interception switched off. A
healthy profile shows nothing, and neither does one whose only shortfall is a
keyword you exempted yourself. When it does appear it says **Some keywords
not intercepted** rather than counting keywords, since the count moved every
time a shortcut was switched on or off and nobody acted on it. The numbers
that do matter, what you exempted and what Chrome refused, are on the line
under it and on the Settings coverage line.
- The `?` shortcut and the **Default AI** setting it read (`settings.defaultAi`).
Pick the assistant with its own keyword instead: `c`, `gpt`, `gem` or `cc`.
- The **AI prompt templates** card. `settings.aiTemplates` still overrides a
Expand All @@ -117,12 +116,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.0.0] - 2026-09-01

Never published anywhere. Recorded as the baseline the 1.1.0 entries are
written against, which is why it has no link below.

### Added

- First release: keyword shortcuts for the address bar via
`declarativeNetRequest`, a shortcut manager (options page), a toolbar
popup, and an omnibox keyword (`bl`).

[Unreleased]: https://github.com/ion05/bunnylol/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/ion05/bunnylol/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/ion05/bunnylol/releases/tag/v1.0.0
[1.1.0]: https://github.com/ion05/bunnylol/releases/tag/v1.1.0
74 changes: 63 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
Bug reports, new shortcuts and fixes are all welcome. Before you start:

- **[AGENTS.md](AGENTS.md) is the architecture note**, and its "Invariants that were violated during
development" section is not decoration. Every entry is a bug that already shipped once. Every one
has a regression test. And every one looks like reasonable code, which is why they came back. Read
it before you touch routing, validation or the override layer.
- **No new dependencies**, devDependencies included. The whole project runs on a handful of build
tools. If you need a helper, inline it.
development" section is not decoration. Every entry is a bug that already shipped once, and every
one looks like reasonable code, which is why they came back. Most carry one regression test, named
in the entry; two carry none and say so. Read it before you touch routing, validation or the
override layer.
- **No new dependencies in what ships.** Nothing is bundled into the extension but this repo's own
source and one font. If you need a helper, inline it. Dev tooling is judged on its own merits and
is currently prettier and eslint on top of typescript, vite and vitest. Adding to that list is a
decision somebody makes on purpose.

## Setup

Expand All @@ -26,11 +29,12 @@ extension card after every build.
## The gate

```bash
pnpm typecheck && pnpm test && pnpm build
pnpm lint && pnpm typecheck && pnpm test && pnpm build
```

All three, green, on **every** commit, not just at the end of a branch. CI runs exactly this on pull
requests, plus `git diff --exit-code -- public/icons store`. `pnpm build` regenerates the icons from
All four, green, on **every** commit, not just at the end of a branch. CI runs exactly this on pull
requests, plus `git diff --exit-code -- public/icons store`. `pnpm lint` goes first because it is
the fastest of the four and the cheapest to fix. `pnpm build` regenerates the icons from
`design/tokens.css`. So if you change the generator or the accent colour and do not commit the
result, it shows up as a dirty tree.

Expand All @@ -46,6 +50,23 @@ the matcher. Then load the extension and try it.
When you add a test, make sure it fails when the thing it guards is broken. Break the code, watch it
go red, put it back.

## The test suite

20 files, about 150 cases, under a second. It is small on purpose. Before you add a test, answer
this: **if it vanished and the code broke, would a user notice?**

It covers `resolve()` turning a typed query into a destination and honouring the `\` and `=` escape,
one or two shapes per smart handler, the redirect rules against real Chrome-generated search URLs,
import and export, the override layer, and a few property tests over the shipped registry (which is
why adding a command usually needs no new test).

It deliberately does not cover stylesheets or design tokens, the DOM a view assembles (the decisions
behind it are pure, in `src/options/model/*.ts`, and those are testable), that a removed feature
stayed removed, or the same rule twice through a wrapper. A table that runs one assertion over every
row of the registry is one property test, not 96 cases: that is how a suite gets to four figures
without covering anything new. Views are verified in a real browser, which is the only place layout
and focus behaviour are visible anyway.

## Adding or changing a command

Commands are plain data in `src/lib/commands.ts`.
Expand Down Expand Up @@ -75,15 +96,24 @@ A shortcut only *you* need does not need a PR at all. Make it in the options pag
- Vanilla TS and CSS in the UI. No framework.
- Colours, sizes and spacing in the UI stylesheets come from `design/tokens.css`. No literal hex, no
raw `font-size: Npx`, and never `color: var(--accent)`, because the sand accent is a fill and
fails contrast as text. `tests/tokens.test.ts` enforces all of it.
fails contrast as text. Reviewed by hand: the 72-case suite that enforced it went with the rest
of the design tests.
- **Comment only where the reason is non-obvious.** Do not restate the code. A comment that says
*why this and not the obvious alternative* is worth more than five that narrate what the next line
does.
- User text reaches the DOM only through `textContent` and `createElement`. A shortcut name is
untrusted input.

There is no linter or formatter, and that is deliberate: one fewer dependency, and one fewer config
to argue with. Match the surrounding code.
`import type`, the indent, the quotes, the semicolons and the ban on default exports are all
enforced now. `pnpm lint` runs eslint and `prettier --check`; `pnpm format` rewrites the files.
Prettier is set to the style already here rather than the other way round, so running it over a
clean tree changes nothing.

Both configs are short and commented. Every rule eslint has switched off names the convention it was
fighting, so if a rule is in your way, read why it is off before turning it back on. Four things are
outside the formatter on purpose: `design/` is the approved design bundle and changes through a
design review, `go.html` carries a deliberately minified inline stylesheet the dispatch page needs
to paint without one, Markdown is hand-wrapped prose, and `pnpm-lock.yaml` belongs to pnpm.

## Pull requests

Expand All @@ -105,3 +135,25 @@ auto-closes the PR that targets it.

Do not open a public issue for a vulnerability. [SECURITY.md](SECURITY.md) has the private reporting
route.

## Maintenance and releases

This project is maintained by one person, [@ion05](https://github.com/ion05). Issues and pull
requests are read, but a reply may take a week. That is the honest expectation rather than a
promise of anything faster.

Versions follow [semantic versioning](https://semver.org), and the stored state format is the
compatibility surface. A new field that older builds ignore is a minor. A change that makes an
older export unreadable is a major. Adding or removing a shipped shortcut is a minor, since a
profile that never touched it still resolves.

A release is:

1. Bump `version` in `package.json` and `public/manifest.json` in the same commit. They are checked
against each other by `tests/manifest.test.ts`, so they cannot drift.
2. Add the section to [CHANGELOG.md](CHANGELOG.md) and the link at the foot of that file.
3. Run the gate, then `pnpm package`, which rebuilds and writes `release/bunnylol-<version>.zip`.
Build fresh rather than trusting a zip already sitting in `release/`: the Web Store enforces
monotonic versions, so uploading a stale build under a new version costs you the next one too.
4. Tag `vX.Y.Z`, push the tag, and attach that zip to a GitHub release.
5. Upload the same zip to the Web Store.
17 changes: 13 additions & 4 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Privacy Policy

Last updated: 2026-09-01
Last updated: 2026-09-03

## Summary

Expand All @@ -20,6 +20,12 @@ The extension also caches its rule-registration status under
until the browser closes and never reaches disk. It holds counts and, when
Chrome rejects a pattern, the affected keywords.

The options page keeps one more value, `bunnylol.collapsed`, in the ordinary
`localStorage` of its own extension page (`COLLAPSE_KEY` in
`src/options/model/collapse.ts`). It is the list of shortcut groups you have
folded on that page, and nothing else. It is per-machine view state rather
than settings, which is why it is not in the exported file.

## What happens when you type in the address bar

BunnyLol registers local `declarativeNetRequest` redirect rules for
Expand All @@ -34,9 +40,12 @@ do not match are left untouched and go to the search engine as normal.
## What the extension cannot see

BunnyLol has no content scripts, reads no page content, and has no access to
your browsing history. It does not request the `tabs` permission. The popup
uses only `chrome.tabs.create` and `chrome.tabs.update`
(`src/popup/popup.ts`), which do not require it.
your browsing history. It does not request the `tabs` permission. Three places
open a tab, and all of them use only `chrome.tabs.create` and
`chrome.tabs.update`, which do not require that permission: the toolbar popup
(`src/popup/popup.ts`), the omnibox keyword (`src/background.ts`), and the
welcome tab shown once on install (`src/lib/install.ts`). Neither call can
read a tab, only point one at a URL.

## Third parties

Expand Down
Loading
Loading