Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
142 changes: 142 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
## Project Overview

Finicky is a native macOS default-browser router. **The single source of truth for the
technical stack, repository layout, and build pipeline is [docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md)** β€”
read it first and do not duplicate that information here.

Project-specific conventions (the rest of this file is generic guidance):

- **Build/test through `scripts/`**, never ad-hoc commands: `./scripts/install.sh`,
`./scripts/build.sh`, `./scripts/test.sh`, `./scripts/dev.sh`. Keep these scripts as the
one place build logic lives.
- **Use `npm`** for the TypeScript packages (`config-api`, `finicky-ui`) β€” the committed
`package-lock.json` is authoritative, despite a stray `packageManager: pnpm` field.
- **Go + cgo/Objective-C**: the macOS app lives in `apps/finicky/src`. Native glue comes in
matched `.go`/`.h`/`.m` sets β€” change them together.
- **User config runs in Goja**, not Node. Don't assume Node/browser globals are available.
- When the stack or build flow changes, update `docs/ARCHITECTURE.md` (one place) rather
than restating details elsewhere.

## Code Quality Principles

<!-- https://github.com/mieweb/template-mieweb-opensource/blob/main/.github/copilot-instructions.md -->

### 🎯 DRY (Don't Repeat Yourself)
- **Never duplicate code**: If you find yourself copying code, extract it into a reusable function
- **Single source of truth**: Each piece of knowledge should have one authoritative representation
- **Refactor mercilessly**: When you see duplication, eliminate it immediately
- **Shared utilities**: Common patterns should be abstracted into utility functions

### πŸ’‹ KISS (Keep It Simple, Stupid)
- **Simple solutions**: Prefer the simplest solution that works
- **Avoid over-engineering**: Don't add complexity for hypothetical future needs
- **Clear naming**: Functions and variables should be self-documenting
- **Small functions**: Break down complex functions into smaller, focused ones
- **Readable code**: Code should be obvious to understand at first glance

### 🧹 Folder Philosophy
- **Clear purpose**: Every folder should have a main thing that anchors its contents.
- **No junk drawers**: Don’t leave loose files without context or explanation.
- **Explain relationships**: If it’s not elegantly obvious how files fit together, add a README or note.
- **Immediate clarity**: Opening a folder should make its organizing principle clear at a glance.

### πŸ”„ Refactoring Guidelines
- **Continuous improvement**: Refactor as you work, not as a separate task
- **Safe refactoring**: Always run tests before and after refactoring
- **Incremental changes**: Make small, safe changes rather than large rewrites
- **Preserve behavior**: Refactoring should not change external behavior
- **Code reviews**: All refactoring should be reviewed for correctness

### ⚰️ Dead Code Management
- **Immediate removal**: Delete unused code immediately when identified
- **Historical preservation**: Move significant dead code to `.attic/` directory with context
- **Documentation**: Include comments explaining why code was moved to attic
- **Regular cleanup**: Review and clean attic directory periodically
- **No accumulation**: Don't let dead code accumulate in active codebase

### 🌐 Testing with MCP Browser
- Use MCP browser in Playwright if available to test functionality
- **Never close the browser** after running MCP browser commands unless explicitly asked
- Let the user interact with the browser after navigation or testing
- Only use `browser_close` when the user specifically requests it

## HTML & CSS Guidelines
- **Semantic Naming**: Every `<div>` and other structural element must use a meaningful, semantic class name that clearly indicates its purpose or role within the layout.
- **CSS Simplicity**: Styles should avoid global resets or overrides that affect unrelated components or default browser behavior. Keep changes scoped and minimal.
- **SASS-First Approach**: All styles should be written in SASS (SCSS) whenever possible. Each component should have its own dedicated SASS file to promote modularity and maintainability.
Comment on lines +63 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify styling approach in finicky-ui
fd -i '.*\.s[ac]ss$' packages/finicky-ui/
fd -i '.*\.css$' packages/finicky-ui/ | head -20
cat packages/finicky-ui/package.json | grep -i sass
cat packages/finicky-ui/vite.config.* | grep -i sass

Repository: johnste/finicky

Length of output: 219


Align the styling guidance with the current CSS setup. packages/finicky-ui uses plain CSS (src/app.css, src/reset.css), so β€œAll styles should be written in SASS (SCSS)” is misleading unless Sass support is added first.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/copilot-instructions.md around lines 63 - 66, The HTML & CSS
guidelines currently say all styles should be written in SASS (SCSS), but the
finicky-ui app is using plain CSS files, so update the guidance to match the
existing setup. In the .github/copilot-instructions.md section for CSS guidance,
adjust the SASS-First Approach text to either recommend plain CSS for now or
explicitly state Sass only if support is added, and keep the rest of the styling
guidance consistent with the src/app.css and src/reset.css structure.


## Accessibility (ARIA Labeling)

### 🎯 Interactive Elements
- **All interactive elements** (buttons, links, forms, dialogs) must include appropriate ARIA roles and labels
- **Use ARIA attributes**: Implement aria-label, aria-labelledby, and aria-describedby to provide clear, descriptive information for screen readers
- **Semantic HTML**: Use semantic HTML wherever possible to enhance accessibility

### πŸ“’ Dynamic Content
- **Announce updates**: Ensure all dynamic content updates (modals, alerts, notifications) are announced to assistive technologies using aria-live regions
- **Maintain tab order**: Maintain logical tab order and keyboard navigation for all features
- **Visible focus**: Provide visible focus indicators for all interactive elements

## Internationalization (I18N)

### 🌍 Text and Language Support
- **Externalize text**: All user-facing text must be externalized for translation
- **Multiple languages**: Support multiple languages, including right-to-left (RTL) languages such as Arabic and Hebrew
- **Language selector**: Provide a language selector for users to choose their preferred language

### πŸ• Localization
- **Format localization**: Ensure date, time, number, and currency formats are localized based on user settings
- **UI compatibility**: Test UI layouts for text expansion and RTL compatibility
- **Unicode support**: Use Unicode throughout to support international character sets

## Documentation Preferences

### Diagrams and Visual Documentation
- **Always use Mermaid diagrams** instead of ASCII art for workflow diagrams, architecture diagrams, and flowcharts
- **Use memorable names** instead of single letters in diagrams (e.g., `Engine`, `Auth`, `Server` instead of `A`, `B`, `C`)
- Use appropriate Mermaid diagram types:
- `graph TB` or `graph LR` for workflow architectures
- `flowchart TD` for process flows
- `sequenceDiagram` for API interactions
- `gitgraph` for branch/release strategies
- Include styling with `classDef` for better visual hierarchy
- Add descriptive comments and emojis sparingly for clarity

### Documentation Standards
- Keep documentation DRY (Don't Repeat Yourself) - reference other docs instead of duplicating
- Use clear cross-references between related documentation files
- Update the main architecture document when workflow structure changes

## Working with GitHub Actions Workflows

### Development Philosophy
- **Script-first approach**: All workflows should call scripts that can be run locally
- **Local development parity**: Developers should be able to run the exact same commands locally as CI runs
- **Simple workflows**: GitHub Actions should be thin wrappers around scripts, not contain complex logic
- **Easy debugging**: When CI fails, developers can reproduce the issue locally by running the same script

## Quick Reference

### πŸͺΆ All Changes should be considered for Pull Request Philosophy

* **Smallest viable change**: Always make the smallest change that fully solves the problem.
* **Fewest files first**: Start with the minimal number of files required.
* **No sweeping edits**: Broad refactors or multi-module changes must be split or proposed as new components.
* **Isolated improvements**: If a change grows complex, extract it into a new function, module, or component instead of modifying multiple areas.
* **Direct requests only**: Large refactors or architectural shifts should only occur when explicitly requested.
* **Update the docs**: Keep documentation in sync with the change in the same PR. When behavior, stack, or build flow changes, update the relevant docs (e.g. [docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md)) rather than leaving them stale.
* **Show UI changes**: Any change to the `finicky-ui` interface must update affected docs/screenshots, and the PR description must include before/after screenshots (or a short clip) of the UI.

### Code Quality Checklist
- [ ] **DRY**: No code duplication - extracted reusable functions?
- [ ] **KISS**: Simplest solution that works?
- [ ] **Minimal Changes**: Smallest viable change made for PR?
- [ ] **Naming**: Self-documenting function/variable names?
- [ ] **Size**: Functions small and focused?
- [ ] **Dead Code**: Removed or archived appropriately?
- [ ] **Accessibility**: ARIA labels and semantic HTML implemented?
- [ ] **I18N**: User-facing text externalized for translation?
- [ ] **Docs**: Documentation updated to match the change?
- [ ] **Screenshots**: UI changes include before/after screenshots in the PR?
- [ ] **Lint**: Run linter if appropriate
- [ ] **Test**: Run tests
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Finicky has browser extensions for Chrome and Firefox. They add an "open with Fi

See [Building Finicky from source](https://github.com/johnste/finicky/wiki/Building-Finicky-from-source)

For a contributor-oriented overview of the codebase and technical stack, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).

### Works well with

If you are looking for something that lets you pick the browser to activate in a graphical interface, check out [Browserosaurus](https://browserosaurus.com/) by Will Stone, an open source browser prompter for macOS. It works really well together with Finicky!
Expand Down
188 changes: 188 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Architecture & Technical Stack

This document gives contributors a fast, high-level map of how Finicky is built so
you can find your way around the repository quickly. For step-by-step build
instructions, see the wiki page
[Building Finicky from source](https://github.com/johnste/finicky/wiki/Building-Finicky-from-source).

## What Finicky is

Finicky is a native **macOS** application that registers itself as the system
default browser. When you click a link anywhere on macOS, the OS hands the URL to
Finicky, which evaluates your JavaScript/TypeScript configuration and opens the
"right" browser (optionally rewriting the URL first).

The whole thing ships as a single self-contained `.app` β€” there is no Node.js
runtime requirement on the user's machine. User config is executed inside an
embedded JavaScript engine compiled into the Go binary.

## Repository layout

```
apps/
finicky/ The macOS application (Go + Objective-C, cgo)
browser-addon/ Chrome/Firefox extension ("Open with Finicky")
packages/
config-api/ TypeScript config API + type definitions (bundled into the app)
finicky-ui/ Svelte web UI shown in the app window (bundled into the app)
example-config/ Example user configs (example.js / example.ts)
scripts/ Build / dev / test / release shell scripts
testdata/ Sample config.js and rules.json for local dev scenarios
docs/ This document
```

## Languages & tooling at a glance

| Area | Stack |
| ------------------- | --------------------------------------------------------------------- |
| macOS app core | **Go 1.24** with **cgo** bridging to **Objective-C** (Cocoa/AppKit) |
| JS config execution | **Goja** (pure-Go JS engine) + **goja-babel** + **esbuild** (Go pkg) |
| Config API | **TypeScript**, bundled with **esbuild**, validated with **Zod** |
| App window UI | **Svelte 5** + **Vite 6** + **TypeScript**, routed via `svelte-routing` |
| Browser extension | **JavaScript**, MV3 (Chrome) / MV2-style (Firefox) manifests |
| Tests | **Go `testing`** (app core), **Vitest** (config-api) |
| Build orchestration | **Bash** scripts in `scripts/` |
| CI | **GitHub Actions** (`.github/workflows/macos-build.yml`) |
| Packaging | Universal binary via `lipo`; signing/notarization via **gon** |

## The macOS app β€” `apps/finicky/src`

The app is written in Go, with the macOS-specific UI/system glue written in
Objective-C and called through cgo. Files come in matched sets, e.g.
[browser.go](../apps/finicky/src/browser.go) /
[browser.h](../apps/finicky/src/browser.h) /
[browser.m](../apps/finicky/src/browser.m), where the `.go` file declares the cgo
bridge and the `.m` file holds the native Cocoa implementation.

Entry point: [main.go](../apps/finicky/src/main.go) (paired with `main.h`/`main.m`).

### Go dependencies ([go.mod](../apps/finicky/src/go.mod))

- `github.com/dop251/goja` β€” JavaScript engine (runs user config without Node).
- `github.com/jvatic/goja-babel` β€” Babel inside Goja, so modern JS/TS syntax works.
- `github.com/evanw/esbuild` β€” used as a Go library to transform/bundle config.
- `github.com/fsnotify/fsnotify` β€” watch the user config file for changes.
- `github.com/Masterminds/semver` β€” version comparisons (e.g. update checks).
- `al.essio.dev/pkg/shellescape` β€” safe shell argument escaping when launching browsers.

### Subpackages

| Package | Responsibility |
| ---------- | ------------------------------------------------------------------------------ |
| `config` | Loads and runs user config. [vm.go](../apps/finicky/src/config/vm.go) drives the Goja VM; also `cache.go`, `configfiles.go`, `console.go`. |
| `resolver` | Core URL resolution pipeline β€” given a URL, decide the browser. (`resolver.go`) |
| `rules` | Matching/rewrite rule evaluation. (`rules.go`) |
| `browser` | Browser detection and launching. `browsers.json`, `detect.go`, `launcher.go`. |
| `shorturl` | Expands short/redirect URLs. `resolver.go`, `shortener_domains.json`. |
| `window` | Native app window (Go + Obj-C). `window.go`/`.h`/`.m`. |
| `util` | Directory helpers and macOS app info via Obj-C. `directories.go`, `info.*`. |
| `logger` | Application logging. `logger.go`. |
| `version` | Version metadata; `commitHash`/`buildDate`/`apiHost` are injected at build time via `-ldflags`. |
| `assets` | Embeds the built UI + config API into the binary (see below). |

### How web assets get into the binary

[assets/assets.go](../apps/finicky/src/assets/assets.go) uses Go's `//go:embed
templates/*` to compile the built Svelte UI directly into the executable. The
build script copies artifacts into `apps/finicky/src/assets/` before `go build`:

- `packages/config-api/dist/finickyConfigAPI.js` β†’ `assets/finickyConfigAPI.js`
(the JS API surface that user configs run against, loaded into Goja).
- `packages/finicky-ui/dist/*` β†’ `assets/templates/` (the embedded UI).

## Config API β€” `packages/config-api`

TypeScript package that defines the public configuration API and the generated
`finicky.d.ts` types users get for autocompletion.

- Build: `esbuild` bundles `src/index.ts` into a single IIFE
(`dist/finickyConfigAPI.js`, global `finickyConfigAPI`, target `es2015`) so it
can be evaluated inside Goja.
- Validation: **Zod** (`configSchema.ts`) + `zod-validation-error`. Types are
generated from the Zod schema via `@duplojs/zod-to-typescript`
(`scripts/generate-typedefs.ts`).
- Tests: **Vitest** (e.g. `config.test.ts`, `FinickyURL.test.ts`, `wildcard.test.ts`).

This package is consumed by the Go app at build time (bundled + embedded), not at
runtime as an npm dependency.

## App UI β€” `packages/finicky-ui`

The window shown when you launch Finicky manually (start page, rules viewer, log
viewer, test-URL tool, about).

- **Svelte 5** components/pages under `src/components` and `src/pages`.
- **Vite 6** build, **TypeScript**, `svelte-routing` for navigation.
- `svelte-check` for type checking.
- Built output (`dist/`) is copied into `assets/templates/` and embedded in the Go
binary.

> Note: the package declares `pnpm` as its `packageManager`, but the build/CI
> scripts currently install and build with **npm** (`package-lock.json` is the
> lockfile used in CI).

## Browser extension β€” `apps/browser-addon`

A small WebExtension that adds an "Open with Finicky" action and lets alt-click
open a link via Finicky. Separate manifests for each browser
(`manifest-chrome.json`, `manifest-firefox.json`) with `background.js` and
`contentScript.js`; `build.sh` packages it.

## Build pipeline β€” `scripts/`

The build is orchestrated by Bash. The key scripts:

- [install.sh](../scripts/install.sh) β€” installs npm deps for both TS packages.
- [build.sh](../scripts/build.sh) β€” the main pipeline:
1. Build `config-api` (esbuild + type generation), copy bundle into the Go assets.
2. Build `finicky-ui` (Vite), copy `dist/*` into `assets/templates/`.
3. `go build` the app with cgo for the target arch(es), injecting version
metadata via `-ldflags`.
4. Assemble the `.app` bundle and copy `finicky.d.ts` + `Contents/` assets.
- Modes: `BUILD_UNIVERSAL=1` builds **arm64 + amd64** and merges them with
`lipo` into a universal binary; `BUILD_TARGET_ARCH` builds a single arch;
otherwise it does a **local native-arch** build and installs to
`/Applications/Finicky.app`.
- [dev.sh](../scripts/dev.sh) β€” runs the already-built app in different config
scenarios (no config, JS only, rules.json only, both) without rebuilding, using
`testdata/`. Supports `FINICKY_MOCK_UPDATE` to preview the update banner.
- [test.sh](../scripts/test.sh) β€” runs `go test ./...` for the app core.
- [watch.sh](../scripts/watch.sh) / [watch-run.sh](../scripts/watch-run.sh) β€”
rebuild/run on change during development.
- [release.sh](../scripts/release.sh) + [gon-config.json](../scripts/gon-config.json)
β€” codesigning and notarization via gon.

## Continuous integration

[.github/workflows/macos-build.yml](../.github/workflows/macos-build.yml) runs on
`macos-latest`:

1. **build-macos** β€” sets up Node 22 + Go 1.24, runs `install.sh`, then
`BUILD_UNIVERSAL=1 build.sh`, and uploads `Finicky-universal.tar.gz`.
2. **sign-and-notarize** β€” on `main` push / manual dispatch, signs and notarizes
the universal app.

(There is also a legacy `nodejs.yml` workflow targeting the old `config-api`
layout/branches.)

## End-to-end flow (clicking a link)

```mermaid
flowchart TD
macosURL[macOS hands URL to Finicky] --> objcAppLayer[Obj-C app layer .m via cgo]
objcAppLayer --> goResolver[Go core: resolver]
goResolver --> configVM[config VM: Goja runs user JS/TS]
configVM --> configApi[config-api bundle + Zod validation]
goResolver --> rulesEngine[rules + shorturl matching/rewrite]
rulesEngine --> browserLauncher[browser: detect + launch chosen browser]
objcAppLayer --> finickyUI[finicky-ui Svelte window for manual launch/troubleshooting]
```

## Where to start as a contributor

- Changing URL matching/rewriting logic β†’ `apps/finicky/src/rules`,
`apps/finicky/src/resolver`, and the `config-api` schema.
- Changing the config API surface or types β†’ `packages/config-api/src`.
- Changing the app window UI β†’ `packages/finicky-ui/src`.
- Native macOS behavior (window, launching, app info) β†’ the `.m`/`.h` files in
`apps/finicky/src` and its `window`/`util` packages.