# zed-bridge A tiny Rust HTTP server that runs on your local workstation at `http://localhost:7654`. Click an **Edit in Zed** button on any remote website you control, and the file opens instantly in the [Zed editor](https://zed.dev) — pointing directly at your already-checked-out copy. Zero external Rust dependencies (only `serde` / `serde_json` for config). Binds to `127.0.0.1` only — nothing reachable from outside your machine. --- ## Why? You run a private or internal site (a [Hugo](https://gohugo.io) blog, a docs site, a CMS, a wiki) hosted on a remote server. The content lives in a git repo you also have checked out locally. When you spot a typo or want to expand a page while browsing the live site, you want to be editing it in Zed **immediately** — not `cd`-ing through directories, guessing paths, or opening your editor manually. zed-bridge lets you drop an **Edit in Zed** button on any page of the remote site. One click and the file is open in your local Zed editor, at the exact path you were just looking at. No SSH, no sync, no finding the file — just edit and go. It's designed for **sites you control** (private dev blogs, internal docs, Hugo preview environments). It is not a generic remote-editing service: the bridge only opens files in repos you've explicitly allowlisted in its config, and it only listens on `127.0.0.1`. --- ## How it works ``` Your browser (remote HTTPS site) │ │ window.open("http://localhost:7654/open?repo=mysite&file=content/posts/foo.md") │ ▼ zed-bridge (systemd user service, always running) │ ├─ validates repo name against ~/.config/zed-bridge/config.json ├─ resolves & canonicalises file path (blocks path traversal) ├─ optional: checks git staleness (local cached state, no network call) └─ launches: zed /home/you/repos/mysite/content/posts/foo.md ``` The pop-up tab shows a result page (success or error) then closes itself after 1.8 s. --- ## Installation ### Prerequisites - [Rust](https://rustup.rs) (stable) - [Zed](https://zed.dev) installed and on `$PATH` (run `zed --version` to confirm) - Linux with systemd (Linux Mint, Ubuntu, Fedora, Arch, etc.) ### Build & install ```bash # Clone or extract this project, then: chmod +x install.sh ./install.sh ``` The script: 1. Runs `cargo build --release` 2. Copies the binary to `~/.local/bin/zed-bridge` 3. Installs the systemd user service 4. Creates `~/.config/zed-bridge/config.json` from the example (if not present) 5. Enables and starts the service ### Manual install (if you prefer) ```bash cargo build --release cp target/release/zed-bridge ~/.local/bin/ cp zed-bridge.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now zed-bridge ``` --- ## Configuration Edit `~/.config/zed-bridge/config.json`: ```json { "port": 7654, "check_git": true, "allowed_origin": null, "repos": { "mysite": "~/repos/mysite", "blog": "~/repos/blog", "docs": "~/repos/docs" } } ``` | Field | Default | Description | |---|---|---| | `port` | `7654` | Local port to listen on | | `check_git` | `true` | Show warning if local branch is behind upstream | | `allowed_origin` | `null` | If set, only requests from this origin are accepted (e.g. `"https://mysite.example.com"`) | | `repos` | `{}` | Map of short name → local path. Supports `~` expansion. | The service reads config on every request, so changes take effect immediately — no restart needed. --- ## Adding Edit buttons to your remote site ### Quickest — plain link ```html ✏️ Edit in Zed ``` This opens a small browser tab that closes itself after 1.8 s. ### Better UX — small pop-up Add this JS once (e.g. in your base layout footer): ```html ``` Then use buttons anywhere: ```html ``` ### Hugo template In a list or single template: ```html {{ if .File }} {{ end }} ``` To restrict to dev/local only: ```html {{ if eq (getenv "HUGO_ENV") "development" }} {{ end }} ``` Or check the request IP in your nginx config and set a header, then gate on it in your template. ### Nginx — show edit buttons only from your own IP In your nginx server block: ```nginx # Only pass the header when the request comes from your workstation/VPN geo $show_edit_button { default 0; 203.0.113.42 1; # your static IP 100.64.0.0/10 1; # Tailscale range } server { ... location / { proxy_set_header X-Dev-Edit $show_edit_button; ... } } ``` Then in your app/template, check `X-Dev-Edit: 1` to conditionally render the buttons. --- ## API reference ### `GET /open` Opens a file in Zed. | Parameter | Required | Description | |---|---|---| | `repo` | ✅ | Repo key from config (e.g. `mysite`) | | `file` | ✅ | Relative path within the repo (e.g. `content/posts/foo.md`) | **Responses:** All return HTML. `200` on success (Zed launched). `400` bad params. `403` forbidden (path traversal or origin check failed). `404` repo/file not found. `500` config error or Zed failed to launch. ### `GET /health` Returns `{"status":"ok","service":"zed-bridge"}` — useful for checking the service is alive from the browser (see the status indicator in `example/edit-button.html`). --- ## Git staleness check When `check_git` is `true`, the bridge runs: ``` git rev-list --left-right --count HEAD...@{upstream} ``` This reads from the **locally cached tracking ref** — it is instant and makes no network calls. The result reflects the state at your last `git fetch` or `git pull`. If your local branch is behind, a yellow warning appears in the result pop-up before Zed opens. The file is still opened — it's a warning, not a block. To disable: set `"check_git": false` in config. --- ## Security - Binds to `127.0.0.1` only — not reachable from the network. - All resolved file paths are canonicalised and checked to be inside the declared repo root before `zed` is called (prevents `../../etc/passwd` style traversal). - `allowed_origin` optionally restricts which website can trigger the bridge. - No shell interpolation — `zed` is called with `Command::new("zed").arg(path)`. --- ## Logs & management ```bash # View live logs journalctl --user -u zed-bridge -f # Restart after editing config or upgrading systemctl --user restart zed-bridge # Stop systemctl --user stop zed-bridge # Disable autostart systemctl --user disable zed-bridge ``` --- ## Troubleshooting **"Zed not found"** — Make sure `zed` is on your `$PATH`. Check with `which zed`. The systemd user service inherits your login session's PATH; if Zed is installed in a non-standard location, add `Environment=PATH=...` to the service file. **Button does nothing / CORS error** — Browsers block `fetch()` to localhost from HTTPS pages (Private Network Access), but `window.open()` still works. Use the pop-up approach shown above, not `fetch()`. **Pop-up blocked** — Some browsers block `window.open` unless triggered directly by a user click. Make sure the `editInZed()` call is inside a click handler, not a timeout. **Service won't start on login** — Ensure systemd user lingering is enabled if you need it to start without an interactive session: ```bash loginctl enable-linger $USER ``` --- ## File structure ``` zed-bridge/ ├── src/ │ └── main.rs # Full server — no async, no HTTP framework ├── example/ │ └── edit-button.html # Live demo + copy-paste snippets ├── Cargo.toml ├── config.example.json ├── zed-bridge.service # systemd user unit ├── install.sh ├── test.sh ├── LICENSE └── README.md ``` --- ## License MIT