Initial public release
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
# 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
|
||||
<a href="http://localhost:7654/open?repo=mysite&file=content/posts/my-post.md"
|
||||
target="_blank" rel="noopener">
|
||||
✏️ Edit in Zed
|
||||
</a>
|
||||
```
|
||||
|
||||
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
|
||||
<script>
|
||||
function editInZed(repo, file) {
|
||||
const url = 'http://localhost:7654/open'
|
||||
+ '?repo=' + encodeURIComponent(repo)
|
||||
+ '&file=' + encodeURIComponent(file);
|
||||
window.open(url, 'zed-bridge',
|
||||
'width=420,height=160,menubar=no,toolbar=no,location=no,status=no');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Then use buttons anywhere:
|
||||
|
||||
```html
|
||||
<button onclick="editInZed('mysite', 'content/posts/my-post.md')">
|
||||
✏️ Edit in Zed
|
||||
</button>
|
||||
```
|
||||
|
||||
### Hugo template
|
||||
|
||||
In a list or single template:
|
||||
|
||||
```html
|
||||
{{ if .File }}
|
||||
<button onclick="editInZed('mysite', '{{ .File.Path }}')">✏️ Edit</button>
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
To restrict to dev/local only:
|
||||
|
||||
```html
|
||||
{{ if eq (getenv "HUGO_ENV") "development" }}
|
||||
<!-- edit button -->
|
||||
{{ 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
|
||||
Reference in New Issue
Block a user